blob: 721565a5e6262f541a6e1e1041b6fb1ae26adf4a [file] [log] [blame]
// 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 '../go_router.dart';
import 'configuration.dart';
import 'misc/errors.dart';
/// The route state during routing.
/// The state contains parsed artifacts of the current URI.
class GoRouterState {
/// Default constructor for creating route state during routing.
const GoRouterState(
this._configuration, {
required this.location,
required this.subloc,
this.params = const <String, String>{},
this.queryParams = const <String, String>{},
this.queryParametersAll = const <String, List<String>>{},
required this.pageKey,
// TODO(johnpryan): remove once namedLocation is removed from go_router.
// See
final RouteConfiguration _configuration;
/// The full location of the route, e.g. /family/f2/person/p1
final String location;
/// The location of this sub-route, e.g. /family/f2
final String subloc;
/// The optional name of the route.
final String? name;
/// The path to this sub-route, e.g. family/:fid
final String? path;
/// The full path to this sub-route, e.g. /family/:fid
final String? fullpath;
/// The parameters for this sub-route, e.g. {'fid': 'f2'}
final Map<String, String> params;
/// The query parameters for the location, e.g. {'from': '/family/f2'}
final Map<String, String> queryParams;
/// The query parameters for the location,
/// e.g. `{'q1': ['v1'], 'q2': ['v2', 'v3']}`
final Map<String, List<String>> queryParametersAll;
/// An extra object to pass along with the navigation.
final Object? extra;
/// The error associated with this sub-route.
final Exception? error;
/// A unique string key for this sub-route, e.g. 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).params['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
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.
'Uses GoRouter.of(context).routeInformationParser.namedLocation instead')
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
}) {
return _configuration.namedLocation(name,
params: params, queryParams: queryParams);
bool operator ==(Object other) {
return other is GoRouterState &&
other.location == location &&
other.subloc == subloc && == name &&
other.path == path &&
other.fullpath == fullpath &&
other.params == params &&
other.queryParams == queryParams &&
other.queryParametersAll == queryParametersAll &&
other.extra == extra &&
other.error == error &&
other.pageKey == pageKey;
int get hashCode => Object.hash(location, subloc, name, path, fullpath,
params, queryParams, queryParametersAll, 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.
class GoRouterStateRegistryScope
extends InheritedNotifier<GoRouterStateRegistry> {
/// Creates a GoRouterStateRegistryScope.
const GoRouterStateRegistryScope({
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.
class GoRouterStateRegistry extends ChangeNotifier {
/// creates a [GoRouterStateRegistry].
/// A [Map] that maps a [Page] to a [GoRouterState].
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);
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 =
} else if (oldPage != page) {
// Need to update the association to avoid memory leak.
_routePageAssociation[route] = page;
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 =
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;
// 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) {