| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:flutter/widgets.dart'; |
| import 'package:meta/meta.dart'; |
| |
| import 'configuration.dart'; |
| import 'misc/errors.dart'; |
| |
| /// The route state during routing. |
| /// |
| /// The state contains parsed artifacts of the current URI. |
| @immutable |
| class GoRouterState { |
| /// Default constructor for creating route state during routing. |
| const GoRouterState( |
| this._configuration, { |
| required this.uri, |
| required this.matchedLocation, |
| this.name, |
| this.path, |
| required this.fullPath, |
| required this.pathParameters, |
| this.extra, |
| this.error, |
| required this.pageKey, |
| }); |
| final RouteConfiguration _configuration; |
| |
| /// The full uri of the route, e.g. /family/f2/person/p1?filter=name#fragment |
| final Uri uri; |
| |
| /// The matched location until this point. |
| /// |
| /// For example: |
| /// |
| /// location = /family/f2/person/p1 |
| /// route = GoRoute('/family/:id') |
| /// |
| /// matchedLocation = /family/f2 |
| final String matchedLocation; |
| |
| /// The optional name of the route associated with this app. |
| /// |
| /// This can be null for GoRouterState pass into top level redirect. |
| final String? name; |
| |
| /// The path of the route associated with this app. e.g. family/:fid |
| /// |
| /// This can be null for GoRouterState pass into top level redirect. |
| final String? path; |
| |
| /// The full path to this sub-route, e.g. /family/:fid |
| /// |
| /// For top level redirect, this is the entire path that matches the location. |
| /// It can be empty if go router can't find a match. In that case, the [error] |
| /// contains more information. |
| final String? fullPath; |
| |
| /// The parameters for this match, e.g. {'fid': 'f2'} |
| final Map<String, String> pathParameters; |
| |
| /// An extra object to pass along with the navigation. |
| final Object? extra; |
| |
| /// The error associated with this sub-route. |
| final GoException? error; |
| |
| /// A unique string key for this sub-route. |
| /// E.g. |
| /// ```dart |
| /// ValueKey('/family/:fid') |
| /// ``` |
| final ValueKey<String> pageKey; |
| |
| /// Gets the [GoRouterState] from context. |
| /// |
| /// The returned [GoRouterState] will depends on which [GoRoute] or |
| /// [ShellRoute] the input `context` is in. |
| /// |
| /// This method only supports [GoRoute] and [ShellRoute] that generate |
| /// [ModalRoute]s. This is typically the case if one uses [GoRoute.builder], |
| /// [ShellRoute.builder], [CupertinoPage], [MaterialPage], |
| /// [CustomTransitionPage], or [NoTransitionPage]. |
| /// |
| /// This method is fine to be called during [GoRoute.builder] or |
| /// [ShellRoute.builder]. |
| /// |
| /// This method cannot be called during [GoRoute.pageBuilder] or |
| /// [ShellRoute.pageBuilder] since there is no [GoRouterState] to be |
| /// associated with. |
| /// |
| /// To access GoRouterState from a widget. |
| /// |
| /// ``` |
| /// GoRoute( |
| /// path: '/:id' |
| /// builder: (_, __) => MyWidget(), |
| /// ); |
| /// |
| /// class MyWidget extends StatelessWidget { |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return Text('${GoRouterState.of(context).pathParameters['id']}'); |
| /// } |
| /// } |
| /// ``` |
| static GoRouterState of(BuildContext context) { |
| final ModalRoute<Object?>? route = ModalRoute.of(context); |
| if (route == null) { |
| throw GoError('There is no modal route above the current context.'); |
| } |
| final RouteSettings settings = route.settings; |
| if (settings is! Page<Object?>) { |
| throw GoError( |
| 'The parent route must be a page route to have a GoRouterState'); |
| } |
| final GoRouterStateRegistryScope? scope = context |
| .dependOnInheritedWidgetOfExactType<GoRouterStateRegistryScope>(); |
| if (scope == null) { |
| throw GoError( |
| 'There is no GoRouterStateRegistryScope above the current context.'); |
| } |
| final GoRouterState state = |
| scope.notifier!._createPageRouteAssociation(settings, route); |
| return state; |
| } |
| |
| /// Get a location from route name and parameters. |
| /// This is useful for redirecting to a named location. |
| String namedLocation( |
| String name, { |
| Map<String, String> pathParameters = const <String, String>{}, |
| Map<String, String> queryParameters = const <String, String>{}, |
| }) { |
| return _configuration.namedLocation(name, |
| pathParameters: pathParameters, queryParameters: queryParameters); |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| return other is GoRouterState && |
| other.uri == uri && |
| other.matchedLocation == matchedLocation && |
| other.name == name && |
| other.path == path && |
| other.fullPath == fullPath && |
| other.pathParameters == pathParameters && |
| other.extra == extra && |
| other.error == error && |
| other.pageKey == pageKey; |
| } |
| |
| @override |
| int get hashCode => Object.hash( |
| uri, |
| matchedLocation, |
| name, |
| path, |
| fullPath, |
| pathParameters, |
| extra, |
| error, |
| pageKey, |
| ); |
| } |
| |
| /// An inherited widget to host a [GoRouterStateRegistry] for the subtree. |
| /// |
| /// Should not be used directly, consider using [GoRouterState.of] to access |
| /// [GoRouterState] from the context. |
| @internal |
| class GoRouterStateRegistryScope |
| extends InheritedNotifier<GoRouterStateRegistry> { |
| /// Creates a GoRouterStateRegistryScope. |
| const GoRouterStateRegistryScope({ |
| super.key, |
| required GoRouterStateRegistry registry, |
| required super.child, |
| }) : super(notifier: registry); |
| } |
| |
| /// A registry to record [GoRouterState] to [Page] relation. |
| /// |
| /// Should not be used directly, consider using [GoRouterState.of] to access |
| /// [GoRouterState] from the context. |
| @internal |
| class GoRouterStateRegistry extends ChangeNotifier { |
| /// creates a [GoRouterStateRegistry]. |
| GoRouterStateRegistry(); |
| |
| /// A [Map] that maps a [Page] to a [GoRouterState]. |
| @visibleForTesting |
| final Map<Page<Object?>, GoRouterState> registry = |
| <Page<Object?>, GoRouterState>{}; |
| |
| final Map<Route<Object?>, Page<Object?>> _routePageAssociation = |
| <ModalRoute<Object?>, Page<Object?>>{}; |
| |
| GoRouterState _createPageRouteAssociation( |
| Page<Object?> page, ModalRoute<Object?> route) { |
| assert(route.settings == page); |
| assert(registry.containsKey(page)); |
| final Page<Object?>? oldPage = _routePageAssociation[route]; |
| if (oldPage == null) { |
| // This is a new association. |
| _routePageAssociation[route] = page; |
| // If there is an association, the registry relies on the route to remove |
| // entry from registry because it wants to preserve the GoRouterState |
| // until the route finishes the popping animations. |
| route.completed.then<void>((Object? result) { |
| // Can't use `page` directly because Route.settings may have changed during |
| // the lifetime of this route. |
| final Page<Object?> associatedPage = |
| _routePageAssociation.remove(route)!; |
| assert(registry.containsKey(associatedPage)); |
| registry.remove(associatedPage); |
| }); |
| } else if (oldPage != page) { |
| // Need to update the association to avoid memory leak. |
| _routePageAssociation[route] = page; |
| assert(registry.containsKey(oldPage)); |
| registry.remove(oldPage); |
| } |
| assert(_routePageAssociation[route] == page); |
| return registry[page]!; |
| } |
| |
| /// Updates this registry with new records. |
| void updateRegistry(Map<Page<Object?>, GoRouterState> newRegistry) { |
| bool shouldNotify = false; |
| final Set<Page<Object?>> pagesWithAssociation = |
| _routePageAssociation.values.toSet(); |
| for (final MapEntry<Page<Object?>, GoRouterState> entry |
| in newRegistry.entries) { |
| final GoRouterState? existingState = registry[entry.key]; |
| if (existingState != null) { |
| if (existingState != entry.value) { |
| shouldNotify = |
| shouldNotify || pagesWithAssociation.contains(entry.key); |
| registry[entry.key] = entry.value; |
| } |
| continue; |
| } |
| // Not in the _registry. |
| registry[entry.key] = entry.value; |
| // Adding or removing registry does not need to notify the listen since |
| // no one should be depending on them. |
| } |
| registry.removeWhere((Page<Object?> key, GoRouterState value) { |
| if (newRegistry.containsKey(key)) { |
| return false; |
| } |
| // For those that have page route association, it will be removed by the |
| // route future. Need to notify the listener so they can update the page |
| // route association if its page has changed. |
| if (pagesWithAssociation.contains(key)) { |
| shouldNotify = true; |
| return false; |
| } |
| return true; |
| }); |
| if (shouldNotify) { |
| notifyListeners(); |
| } |
| } |
| } |