[go_router] Refactors imperative APIs and browser history (#4134)

Several thing.

1. I move all the imperative logic from RouterDelegate to RouteInformationParser, so that the imperative API can go through Router parsing pipeline. The Parser will handle modifying mutating RouteMatchList and produce the final RouteMatchList. The RouterDelegate would only focus on building the widget base on the final RouteMatchList 
2. combine RouteMatcher and Redirector with RouteConfiguration. I feel that instead of passing three class instances around, we should probably just have one class for all the route parsing related utility.
3. serialize routeMatchList and store into browser history. This way we can let backward and forward button to reflect imperative operation as well.
4. Some minor clean ups
This commit is contained in:
chunhtai
2023-06-07 16:19:52 -07:00
committed by GitHub
parent e37dd83c91
commit 010ba50128
24 changed files with 1622 additions and 1476 deletions

View File

@ -1,6 +1,13 @@
## 8.0.0
- **BREAKING CHANGE**:
- Imperatively pushed GoRoute no longer change URL.
- Browser backward and forward button respects imperative route operations.
- Refactors the route parsing pipeline.
## 7.1.1
* Removes obsolete null checks on non-nullable values.
- Removes obsolete null checks on non-nullable values.
## 7.1.0

View File

@ -37,7 +37,8 @@ See the API documentation for details on the following topics:
- [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html)
## Migration guides
- [Migrating to 7.0.0](https://docs.google.com/document/d/10Xbpifbs4E-zh6YE5akIO8raJq_m3FIXs6nUGdOspOg).
- [Migrating to 8.0.0](https://flutter.dev/go/go-router-v8-breaking-changes).
- [Migrating to 7.0.0](https://flutter.dev/go/go-router-v7-breaking-changes).
- [Migrating to 6.0.0](https://flutter.dev/go/go-router-v6-breaking-changes)
- [Migrating to 5.1.2](https://flutter.dev/go/go-router-v5-1-2-breaking-changes)
- [Migrating to 5.0](https://flutter.dev/go/go-router-v5-breaking-changes)

View File

@ -9,8 +9,8 @@ import '../go_router.dart';
import 'configuration.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
import 'misc/error_screen.dart';
import 'misc/errors.dart';
import 'pages/cupertino.dart';
import 'pages/material.dart';
import 'route_data.dart';
@ -83,7 +83,7 @@ class RouteBuilder {
RouteMatchList matchList,
bool routerNeglect,
) {
if (matchList.isEmpty) {
if (matchList.isEmpty && !matchList.isError) {
// The build method can be called before async redirect finishes. Build a
// empty box until then.
return const SizedBox.shrink();
@ -92,18 +92,12 @@ class RouteBuilder {
context,
Builder(
builder: (BuildContext context) {
try {
final Map<Page<Object?>, GoRouterState> newRegistry =
<Page<Object?>, GoRouterState>{};
final Widget result = tryBuild(context, matchList, routerNeglect,
configuration.navigatorKey, newRegistry);
_registry.updateRegistry(newRegistry);
return GoRouterStateRegistryScope(
registry: _registry, child: result);
} on _RouteBuilderError catch (e) {
return _buildErrorNavigator(context, e, matchList.uri,
onPopPageWithRouteMatch, configuration.navigatorKey);
}
final Map<Page<Object?>, GoRouterState> newRegistry =
<Page<Object?>, GoRouterState>{};
final Widget result = tryBuild(context, matchList, routerNeglect,
configuration.navigatorKey, newRegistry);
_registry.updateRegistry(newRegistry);
return GoRouterStateRegistryScope(registry: _registry, child: result);
},
),
);
@ -147,28 +141,31 @@ class RouteBuilder {
bool routerNeglect,
GlobalKey<NavigatorState> navigatorKey,
Map<Page<Object?>, GoRouterState> registry) {
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
<GlobalKey<NavigatorState>, List<Page<Object?>>>{};
try {
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage;
if (matchList.isError) {
keyToPage = <GlobalKey<NavigatorState>, List<Page<Object?>>>{
navigatorKey: <Page<Object?>>[
_buildErrorPage(
context, _buildErrorState(matchList.error!, matchList.uri)),
]
};
} else {
keyToPage = <GlobalKey<NavigatorState>, List<Page<Object?>>>{};
_buildRecursive(context, matchList, 0, pagePopContext, routerNeglect,
keyToPage, navigatorKey, registry);
// Every Page should have a corresponding RouteMatch.
assert(keyToPage.values.flattened.every((Page<Object?> page) =>
pagePopContext.getRouteMatchForPage(page) != null));
return keyToPage[navigatorKey]!;
} on _RouteBuilderError catch (e) {
return <Page<Object?>>[
_buildErrorPage(context, e, matchList.uri),
];
} finally {
/// Clean up previous cache to prevent memory leak, making sure any nested
/// stateful shell routes for the current match list are kept.
final Set<Key> activeKeys = keyToPage.keys.toSet()
..addAll(_nestedStatefulNavigatorKeys(matchList));
_goHeroCache.removeWhere(
(GlobalKey<NavigatorState> key, _) => !activeKeys.contains(key));
}
/// Clean up previous cache to prevent memory leak, making sure any nested
/// stateful shell routes for the current match list are kept.
final Set<Key> activeKeys = keyToPage.keys.toSet()
..addAll(_nestedStatefulNavigatorKeys(matchList));
_goHeroCache.removeWhere(
(GlobalKey<NavigatorState> key, _) => !activeKeys.contains(key));
return keyToPage[navigatorKey]!;
}
static Set<GlobalKey<NavigatorState>> _nestedStatefulNavigatorKeys(
@ -200,15 +197,15 @@ class RouteBuilder {
}
final RouteMatch match = matchList.matches[startIndex];
if (match.error != null) {
throw _RouteBuilderError('Match error found during build phase',
exception: match.error);
}
final RouteBase route = match.route;
final GoRouterState state = buildState(matchList, match);
Page<Object?>? page;
if (route is GoRoute) {
if (state.error != null) {
page = _buildErrorPage(context, state);
keyToPages.putIfAbsent(navigatorKey, () => <Page<Object?>>[]).add(page);
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
routerNeglect, keyToPages, navigatorKey, registry);
} else if (route is GoRoute) {
page = _buildPageForGoRoute(context, state, match, route, pagePopContext);
// If this GoRoute is for a different Navigator, add it to the
// list of out of scope pages
@ -284,7 +281,7 @@ class RouteBuilder {
registry[page] = state;
pagePopContext._setRouteMatchForPage(page, match);
} else {
throw _RouteBuilderException('Unsupported route type $route');
throw GoError('Unsupported route type $route');
}
}
@ -324,8 +321,17 @@ class RouteBuilder {
name = route.name;
path = route.path;
}
final RouteMatchList effectiveMatchList =
match is ImperativeRouteMatch ? match.matches : matchList;
final RouteMatchList effectiveMatchList;
if (match is ImperativeRouteMatch) {
effectiveMatchList = match.matches;
if (effectiveMatchList.isError) {
return _buildErrorState(
effectiveMatchList.error!, effectiveMatchList.uri);
}
} else {
effectiveMatchList = matchList;
assert(!effectiveMatchList.isError);
}
return GoRouterState(
configuration,
location: effectiveMatchList.uri.toString(),
@ -335,10 +341,10 @@ class RouteBuilder {
fullPath: effectiveMatchList.fullPath,
pathParameters:
Map<String, String>.from(effectiveMatchList.pathParameters),
error: match.error,
error: effectiveMatchList.error,
queryParameters: effectiveMatchList.uri.queryParameters,
queryParametersAll: effectiveMatchList.uri.queryParametersAll,
extra: match.extra,
extra: effectiveMatchList.extra,
pageKey: match.pageKey,
);
}
@ -370,7 +376,7 @@ class RouteBuilder {
final GoRouterWidgetBuilder? builder = route.builder;
if (builder == null) {
throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route');
throw GoError('No routeBuilder provided to GoRoute: $route');
}
return builder(context, state);
@ -405,7 +411,7 @@ class RouteBuilder {
final Widget? widget =
route.buildWidget(context, state, shellRouteContext!);
if (widget == null) {
throw _RouteBuilderError('No builder provided to ShellRoute: $route');
throw GoError('No builder provided to ShellRoute: $route');
}
return widget;
@ -485,38 +491,26 @@ class RouteBuilder {
child: child,
);
/// Builds a Navigator containing an error page.
Widget _buildErrorNavigator(
BuildContext context,
_RouteBuilderError e,
Uri uri,
PopPageWithRouteMatchCallback onPopPage,
GlobalKey<NavigatorState> navigatorKey) {
return _buildNavigator(
(Route<dynamic> route, dynamic result) => onPopPage(route, result, null),
<Page<Object?>>[
_buildErrorPage(context, e, uri),
],
navigatorKey,
);
}
/// Builds a an error page.
Page<void> _buildErrorPage(
BuildContext context,
_RouteBuilderError error,
GoRouterState _buildErrorState(
Exception error,
Uri uri,
) {
final GoRouterState state = GoRouterState(
final String location = uri.toString();
return GoRouterState(
configuration,
location: uri.toString(),
location: location,
matchedLocation: uri.path,
name: null,
queryParameters: uri.queryParameters,
queryParametersAll: uri.queryParametersAll,
error: Exception(error),
pageKey: const ValueKey<String>('error'),
error: error,
pageKey: ValueKey<String>('$location(error)'),
);
}
/// Builds a an error page.
Page<void> _buildErrorPage(BuildContext context, GoRouterState state) {
assert(state.error != null);
// If the error page builder is provided, use that, otherwise, if the error
// builder is provided, wrap that in an app-specific page (for example,
@ -556,43 +550,6 @@ typedef _PageBuilderForAppType = Page<void> Function({
required Widget child,
});
/// An error that occurred while building the app's UI based on the route
/// matches.
class _RouteBuilderError extends Error {
/// Constructs a [_RouteBuilderError].
_RouteBuilderError(this.message, {this.exception});
/// The error message.
final String message;
/// The exception that occurred.
final Exception? exception;
@override
String toString() {
return '$message ${exception ?? ""}';
}
}
/// An error that occurred while building the app's UI based on the route
/// matches.
class _RouteBuilderException implements Exception {
/// Constructs a [_RouteBuilderException].
//ignore: unused_element
_RouteBuilderException(this.message, {this.exception});
/// The error message.
final String message;
/// The exception that occurred.
final Exception? exception;
@override
String toString() {
return '$message ${exception ?? ""}';
}
}
/// Context used to provide a route to page association when popping routes.
class _PagePopContext {
_PagePopContext._(this.onPopPageWithRouteMatch);

View File

@ -2,11 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'logging.dart';
import 'matching.dart';
import 'match.dart';
import 'misc/errors.dart';
import 'path_utils.dart';
import 'typedefs.dart';
@ -26,8 +28,7 @@ class RouteConfiguration {
_debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
assert(_debugCheckParentNavigatorKeys(
routes, <GlobalKey<NavigatorState>>[navigatorKey])) {
assert(_debugCheckStatefulShellBranchDefaultLocations(
routes, RouteMatcher(this)));
assert(_debugCheckStatefulShellBranchDefaultLocations(routes));
_cacheNameToPath('', routes);
log.info(debugKnownRoutes());
}
@ -37,11 +38,13 @@ class RouteConfiguration {
late bool subRouteIsTopLevel;
if (route is GoRoute) {
if (isTopLevel) {
assert(route.path.startsWith('/'),
'top-level path must start with "/": $route');
if (!route.path.startsWith('/')) {
throw GoError('top-level path must start with "/": $route');
}
} else {
assert(!route.path.startsWith('/') && !route.path.endsWith('/'),
'sub-route path may not start or end with /: $route');
if (route.path.startsWith('/') || route.path.endsWith('/')) {
throw GoError('sub-route path may not start or end with /: $route');
}
}
subRouteIsTopLevel = false;
} else if (route is ShellRouteBase) {
@ -62,11 +65,11 @@ class RouteConfiguration {
if (parentKey != null) {
// Verify that the root navigator or a ShellRoute ancestor has a
// matching navigator key.
assert(
allowedKeys.contains(parentKey),
'parentNavigatorKey $parentKey must refer to'
" an ancestor ShellRoute's navigatorKey or GoRouter's"
' navigatorKey');
if (!allowedKeys.contains(parentKey)) {
throw GoError('parentNavigatorKey $parentKey must refer to'
" an ancestor ShellRoute's navigatorKey or GoRouter's"
' navigatorKey');
}
_debugCheckParentNavigatorKeys(
route.routes,
@ -91,10 +94,11 @@ class RouteConfiguration {
);
} else if (route is StatefulShellRoute) {
for (final StatefulShellBranch branch in route.branches) {
assert(
!allowedKeys.contains(branch.navigatorKey),
'StatefulShellBranch must not reuse an ancestor navigatorKey '
'(${branch.navigatorKey})');
if (allowedKeys.contains(branch.navigatorKey)) {
throw GoError(
'StatefulShellBranch must not reuse an ancestor navigatorKey '
'(${branch.navigatorKey})');
}
_debugCheckParentNavigatorKeys(
branch.routes,
@ -131,66 +135,77 @@ class RouteConfiguration {
// Check to see that the configured initialLocation of StatefulShellBranches
// points to a descendant route of the route branch.
bool _debugCheckStatefulShellBranchDefaultLocations(
List<RouteBase> routes, RouteMatcher matcher) {
try {
for (final RouteBase route in routes) {
if (route is StatefulShellRoute) {
for (final StatefulShellBranch branch in route.branches) {
if (branch.initialLocation == null) {
// Recursively search for the first GoRoute descendant. Will
// throw assertion error if not found.
final GoRoute? route = branch.defaultRoute;
final String? initialLocation =
route != null ? locationForRoute(route) : null;
assert(
initialLocation != null,
bool _debugCheckStatefulShellBranchDefaultLocations(List<RouteBase> routes) {
for (final RouteBase route in routes) {
if (route is StatefulShellRoute) {
for (final StatefulShellBranch branch in route.branches) {
if (branch.initialLocation == null) {
// Recursively search for the first GoRoute descendant. Will
// throw assertion error if not found.
final GoRoute? route = branch.defaultRoute;
final String? initialLocation =
route != null ? locationForRoute(route) : null;
if (initialLocation == null) {
throw GoError(
'The default location of a StatefulShellBranch must be '
'derivable from GoRoute descendant');
assert(
route!.pathParameters.isEmpty,
}
if (route!.pathParameters.isNotEmpty) {
throw GoError(
'The default location of a StatefulShellBranch cannot be '
'a parameterized route');
} else {
final List<RouteBase> matchRoutes =
matcher.findMatch(branch.initialLocation!).routes;
final int shellIndex = matchRoutes.indexOf(route);
bool matchFound = false;
if (shellIndex >= 0 && (shellIndex + 1) < matchRoutes.length) {
final RouteBase branchRoot = matchRoutes[shellIndex + 1];
matchFound = branch.routes.contains(branchRoot);
}
assert(
matchFound,
}
} else {
final RouteMatchList matchList = findMatch(branch.initialLocation!);
if (matchList.isError) {
throw GoError(
'initialLocation (${matchList.uri}) of StatefulShellBranch must '
'be a valid location');
}
final List<RouteBase> matchRoutes = matchList.routes;
final int shellIndex = matchRoutes.indexOf(route);
bool matchFound = false;
if (shellIndex >= 0 && (shellIndex + 1) < matchRoutes.length) {
final RouteBase branchRoot = matchRoutes[shellIndex + 1];
matchFound = branch.routes.contains(branchRoot);
}
if (!matchFound) {
throw GoError(
'The initialLocation (${branch.initialLocation}) of '
'StatefulShellBranch must match a descendant route of the '
'branch');
}
}
}
_debugCheckStatefulShellBranchDefaultLocations(route.routes, matcher);
}
} on MatcherError catch (e) {
assert(
false,
'initialLocation (${e.location}) of StatefulShellBranch must '
'be a valid location');
_debugCheckStatefulShellBranchDefaultLocations(route.routes);
}
return true;
}
/// The match used when there is an error during parsing.
static RouteMatchList _errorRouteMatchList(Uri uri, String errorMessage) {
final Exception error = Exception(errorMessage);
return RouteMatchList(
matches: const <RouteMatch>[],
error: error,
uri: uri,
pathParameters: const <String, String>{},
);
}
/// The list of top level routes used by [GoRouterDelegate].
final List<RouteBase> routes;
/// The limit for the number of consecutive redirects.
final int redirectLimit;
/// The global key for top level navigator.
final GlobalKey<NavigatorState> navigatorKey;
/// Top level page redirect.
final GoRouterRedirect topRedirect;
/// The key to use when building the root [Navigator].
final GlobalKey<NavigatorState> navigatorKey;
final Map<String, String> _nameToPath = <String, String>{};
/// Looks up the url location by a [GoRoute]'s name.
@ -235,6 +250,269 @@ class RouteConfiguration {
.toString();
}
/// Finds the routes that matched the given URL.
RouteMatchList findMatch(String location, {Object? extra}) {
final Uri uri = Uri.parse(canonicalUri(location));
final Map<String, String> pathParameters = <String, String>{};
final List<RouteMatch>? matches = _getLocRouteMatches(uri, pathParameters);
if (matches == null) {
return _errorRouteMatchList(uri, 'no routes for location: $uri');
}
return RouteMatchList(
matches: matches,
uri: uri,
pathParameters: pathParameters,
extra: extra);
}
List<RouteMatch>? _getLocRouteMatches(
Uri uri, Map<String, String> pathParameters) {
final List<RouteMatch>? result = _getLocRouteRecursively(
location: uri.path,
remainingLocation: uri.path,
matchedLocation: '',
pathParameters: pathParameters,
routes: routes,
);
return result;
}
List<RouteMatch>? _getLocRouteRecursively({
required String location,
required String remainingLocation,
required String matchedLocation,
required Map<String, String> pathParameters,
required List<RouteBase> routes,
}) {
List<RouteMatch>? result;
late Map<String, String> subPathParameters;
// find the set of matches at this level of the tree
for (final RouteBase route in routes) {
subPathParameters = <String, String>{};
final RouteMatch? match = RouteMatch.match(
route: route,
remainingLocation: remainingLocation,
matchedLocation: matchedLocation,
pathParameters: subPathParameters,
);
if (match == null) {
continue;
}
if (match.route is GoRoute &&
match.matchedLocation.toLowerCase() == location.toLowerCase()) {
// If it is a complete match, then return the matched route
// NOTE: need a lower case match because matchedLocation is canonicalized to match
// the path case whereas the location can be of any case and still match
result = <RouteMatch>[match];
} else if (route.routes.isEmpty) {
// If it is partial match but no sub-routes, bail.
continue;
} else {
// Otherwise, recurse
final String childRestLoc;
final String newParentSubLoc;
if (match.route is ShellRouteBase) {
childRestLoc = remainingLocation;
newParentSubLoc = matchedLocation;
} else {
assert(location.startsWith(match.matchedLocation));
assert(remainingLocation.isNotEmpty);
childRestLoc = location.substring(match.matchedLocation.length +
(match.matchedLocation == '/' ? 0 : 1));
newParentSubLoc = match.matchedLocation;
}
final List<RouteMatch>? subRouteMatch = _getLocRouteRecursively(
location: location,
remainingLocation: childRestLoc,
matchedLocation: newParentSubLoc,
pathParameters: subPathParameters,
routes: route.routes,
);
// If there's no sub-route matches, there is no match for this location
if (subRouteMatch == null) {
continue;
}
result = <RouteMatch>[match, ...subRouteMatch];
}
// Should only reach here if there is a match.
break;
}
if (result != null) {
pathParameters.addAll(subPathParameters);
}
return result;
}
/// Processes redirects by returning a new [RouteMatchList] representing the new
/// location.
FutureOr<RouteMatchList> redirect(
BuildContext context, FutureOr<RouteMatchList> prevMatchListFuture,
{required List<RouteMatchList> redirectHistory}) {
FutureOr<RouteMatchList> processRedirect(RouteMatchList prevMatchList) {
final String prevLocation = prevMatchList.uri.toString();
FutureOr<RouteMatchList> processTopLevelRedirect(
String? topRedirectLocation) {
if (topRedirectLocation != null &&
topRedirectLocation != prevLocation) {
final RouteMatchList newMatch = _getNewMatches(
topRedirectLocation,
prevMatchList.uri,
redirectHistory,
);
if (newMatch.isError) {
return newMatch;
}
return redirect(
context,
newMatch,
redirectHistory: redirectHistory,
);
}
FutureOr<RouteMatchList> processRouteLevelRedirect(
String? routeRedirectLocation) {
if (routeRedirectLocation != null &&
routeRedirectLocation != prevLocation) {
final RouteMatchList newMatch = _getNewMatches(
routeRedirectLocation,
prevMatchList.uri,
redirectHistory,
);
if (newMatch.isError) {
return newMatch;
}
return redirect(
context,
newMatch,
redirectHistory: redirectHistory,
);
}
return prevMatchList;
}
final FutureOr<String?> routeLevelRedirectResult =
_getRouteLevelRedirect(context, prevMatchList, 0);
if (routeLevelRedirectResult is String?) {
return processRouteLevelRedirect(routeLevelRedirectResult);
}
return routeLevelRedirectResult
.then<RouteMatchList>(processRouteLevelRedirect);
}
redirectHistory.add(prevMatchList);
// Check for top-level redirect
final FutureOr<String?> topRedirectResult = topRedirect(
context,
GoRouterState(
this,
location: prevLocation,
name: null,
// No name available at the top level trim the query params off the
// sub-location to match route.redirect
matchedLocation: prevMatchList.uri.path,
queryParameters: prevMatchList.uri.queryParameters,
queryParametersAll: prevMatchList.uri.queryParametersAll,
extra: prevMatchList.extra,
pageKey: const ValueKey<String>('topLevel'),
),
);
if (topRedirectResult is String?) {
return processTopLevelRedirect(topRedirectResult);
}
return topRedirectResult.then<RouteMatchList>(processTopLevelRedirect);
}
if (prevMatchListFuture is RouteMatchList) {
return processRedirect(prevMatchListFuture);
}
return prevMatchListFuture.then<RouteMatchList>(processRedirect);
}
FutureOr<String?> _getRouteLevelRedirect(
BuildContext context,
RouteMatchList matchList,
int currentCheckIndex,
) {
if (currentCheckIndex >= matchList.matches.length) {
return null;
}
final RouteMatch match = matchList.matches[currentCheckIndex];
FutureOr<String?> processRouteRedirect(String? newLocation) =>
newLocation ??
_getRouteLevelRedirect(context, matchList, currentCheckIndex + 1);
final RouteBase route = match.route;
FutureOr<String?> routeRedirectResult;
if (route is GoRoute && route.redirect != null) {
routeRedirectResult = route.redirect!(
context,
GoRouterState(
this,
location: matchList.uri.toString(),
matchedLocation: match.matchedLocation,
name: route.name,
path: route.path,
fullPath: matchList.fullPath,
extra: matchList.extra,
pathParameters: matchList.pathParameters,
queryParameters: matchList.uri.queryParameters,
queryParametersAll: matchList.uri.queryParametersAll,
pageKey: match.pageKey,
),
);
}
if (routeRedirectResult is String?) {
return processRouteRedirect(routeRedirectResult);
}
return routeRedirectResult.then<String?>(processRouteRedirect);
}
RouteMatchList _getNewMatches(
String newLocation,
Uri previousLocation,
List<RouteMatchList> redirectHistory,
) {
try {
final RouteMatchList newMatch = findMatch(newLocation);
_addRedirect(redirectHistory, newMatch, previousLocation);
return newMatch;
} on RedirectionError catch (e) {
log.info('Redirection error: ${e.message}');
return _errorRouteMatchList(e.location, e.message);
}
}
/// Adds the redirect to [redirects] if it is valid.
///
/// Throws if a loop is detected or the redirection limit is reached.
void _addRedirect(
List<RouteMatchList> redirects,
RouteMatchList newMatch,
Uri prevLocation,
) {
if (redirects.contains(newMatch)) {
throw RedirectionError('redirect loop detected',
<RouteMatchList>[...redirects, newMatch], prevLocation);
}
if (redirects.length > redirectLimit) {
throw RedirectionError('too many redirects',
<RouteMatchList>[...redirects, newMatch], prevLocation);
}
redirects.add(newMatch);
log.info('redirecting to $newMatch');
}
/// Get the location for the provided route.
///
/// Builds the absolute path for the route, by concatenating the paths of the

View File

@ -10,7 +10,6 @@ import 'package:flutter/widgets.dart';
import 'builder.dart';
import 'configuration.dart';
import 'match.dart';
import 'matching.dart';
import 'misc/errors.dart';
import 'typedefs.dart';
@ -46,25 +45,10 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
/// Set to true to disable creating history entries on the web.
final bool routerNeglect;
RouteMatchList _matchList = RouteMatchList.empty;
final RouteConfiguration _configuration;
/// Stores the number of times each route route has been pushed.
///
/// This is used to generate a unique key for each route.
///
/// For example, it could be equal to:
/// ```dart
/// {
/// 'family': 1,
/// 'family/:fid': 2,
/// }
/// ```
final Map<String, int> _pushCounts = <String, int>{};
_NavigatorStateIterator _createNavigatorStateIterator() =>
_NavigatorStateIterator(_matchList, navigatorKey.currentState!);
_NavigatorStateIterator(currentConfiguration, navigatorKey.currentState!);
@override
Future<bool> popRoute() async {
@ -78,45 +62,6 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
return false;
}
ValueKey<String> _getNewKeyForPath(String path) {
// Remap the pageKey to allow any number of the same page on the stack
final int count = (_pushCounts[path] ?? -1) + 1;
_pushCounts[path] = count;
return ValueKey<String>('$path-p$count');
}
Future<T?> _push<T extends Object?>(
RouteMatchList matches, ValueKey<String> pageKey) async {
final ImperativeRouteMatch<T> newPageKeyMatch = ImperativeRouteMatch<T>(
pageKey: pageKey,
matches: matches,
);
_matchList = _matchList.push(newPageKeyMatch);
return newPageKeyMatch.future;
}
void _remove(RouteMatch match) {
_matchList = _matchList.remove(match);
}
/// Pushes the given location onto the page stack.
///
/// See also:
/// * [pushReplacement] which replaces the top-most page of the page stack and
/// always use a new page key.
/// * [replace] which replaces the top-most page of the page stack but treats
/// it as the same page. The page key will be reused. This will preserve the
/// state and not run any page animation.
Future<T?> push<T extends Object?>(RouteMatchList matches) async {
assert(matches.last.route is! ShellRoute);
final ValueKey<String> pageKey = _getNewKeyForPath(matches.fullPath);
final Future<T?> future = _push(matches, pageKey);
notifyListeners();
return future;
}
/// Returns `true` if the active Navigator can pop.
bool canPop() {
final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
@ -142,7 +87,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
void _debugAssertMatchListNotEmpty() {
assert(
_matchList.isNotEmpty,
currentConfiguration.isNotEmpty,
'You have popped the last page off of the stack,'
' there are no pages left to show',
);
@ -157,7 +102,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
if (match is ImperativeRouteMatch) {
match.complete(result);
}
_remove(match!);
currentConfiguration = currentConfiguration.remove(match!);
notifyListeners();
assert(() {
_debugAssertMatchListNotEmpty();
@ -166,57 +111,19 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
return true;
}
/// Replaces the top-most page of the page stack with the given one.
///
/// The page key of the new page will always be different from the old one.
///
/// See also:
/// * [push] which pushes the given location onto the page stack.
/// * [replace] which replaces the top-most page of the page stack but treats
/// it as the same page. The page key will be reused. This will preserve the
/// state and not run any page animation.
void pushReplacement(RouteMatchList matches) {
assert(matches.last.route is! ShellRoute);
_remove(_matchList.last);
push(matches); // [push] will notify the listeners.
}
/// Replaces the top-most page of the page stack with the given one but treats
/// it as the same page.
///
/// The page key will be reused. This will preserve the state and not run any
/// page animation.
///
/// See also:
/// * [push] which pushes the given location onto the page stack.
/// * [pushReplacement] which replaces the top-most page of the page stack but
/// always uses a new page key.
void replace(RouteMatchList matches) {
assert(matches.last.route is! ShellRoute);
final RouteMatch routeMatch = _matchList.last;
final ValueKey<String> pageKey = routeMatch.pageKey;
_remove(routeMatch);
_push(matches, pageKey);
notifyListeners();
}
/// For internal use; visible for testing only.
@visibleForTesting
RouteMatchList get matches => _matchList;
/// For use by the Router architecture as part of the RouterDelegate.
GlobalKey<NavigatorState> get navigatorKey => _configuration.navigatorKey;
/// For use by the Router architecture as part of the RouterDelegate.
@override
RouteMatchList get currentConfiguration => _matchList;
RouteMatchList currentConfiguration = RouteMatchList.empty;
/// For use by the Router architecture as part of the RouterDelegate.
@override
Widget build(BuildContext context) {
return builder.build(
context,
_matchList,
currentConfiguration,
routerNeglect,
);
}
@ -224,8 +131,8 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
/// For use by the Router architecture as part of the RouterDelegate.
@override
Future<void> setNewRoutePath(RouteMatchList configuration) {
_matchList = configuration;
assert(_matchList.isNotEmpty);
currentConfiguration = configuration;
assert(currentConfiguration.isNotEmpty || currentConfiguration.isError);
notifyListeners();
// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking

View File

@ -2,79 +2,229 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'match.dart';
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore_for_file: deprecated_member_use
/// The type of the navigation.
///
/// This enum is used by [RouteInformationState] to denote the navigation
/// operations.
enum NavigatingType {
/// Push new location on top of the [RouteInformationState.baseRouteMatchList].
push,
/// Push new location and remove top-most [RouteMatch] of the
/// [RouteInformationState.baseRouteMatchList].
pushReplacement,
/// Push new location and replace top-most [RouteMatch] of the
/// [RouteInformationState.baseRouteMatchList].
replace,
/// Replace the entire [RouteMatchList] with the new location.
go,
}
/// The data class to be stored in [RouteInformation.state] to be used by
/// [GoRouteInformationPrarser].
///
/// This state class is used internally in go_router and will not be send to
/// the engine.
class RouteInformationState<T> {
/// Creates an InternalRouteInformationState.
@visibleForTesting
RouteInformationState({
this.extra,
this.completer,
this.baseRouteMatchList,
required this.type,
}) : assert((type != NavigatingType.go) ==
(completer != null && baseRouteMatchList != null));
/// The extra object used when navigating with [GoRouter].
final Object? extra;
/// The completer that needs to be complete when the newly added route is
/// popped off the screen..
///
/// This is only null if [type] is [NavigatingType.go].
final Completer<T?>? completer;
/// The base route match list to push on top to.
///
/// This is only null if [type] is [NavigatingType.go].
final RouteMatchList? baseRouteMatchList;
/// The type of navigation.
final NavigatingType type;
}
/// The [RouteInformationProvider] created by go_router.
class GoRouteInformationProvider extends RouteInformationProvider
with WidgetsBindingObserver, ChangeNotifier {
/// Creates a [GoRouteInformationProvider].
GoRouteInformationProvider({
required RouteInformation initialRouteInformation,
required String initialLocation,
required Object? initialExtra,
Listenable? refreshListenable,
}) : _refreshListenable = refreshListenable,
_value = initialRouteInformation {
_value = RouteInformation(
location: initialLocation,
state: RouteInformationState<void>(
extra: initialExtra, type: NavigatingType.go),
),
_valueInEngine = _kEmptyRouteInformation {
_refreshListenable?.addListener(notifyListeners);
}
final Listenable? _refreshListenable;
static WidgetsBinding get _binding => WidgetsBinding.instance;
static const RouteInformation _kEmptyRouteInformation =
RouteInformation(location: '');
@override
void routerReportsNewRouteInformation(RouteInformation routeInformation,
{RouteInformationReportingType type =
RouteInformationReportingType.none}) {
// Avoid adding a new history entry if the route is the same as before.
final bool replace = type == RouteInformationReportingType.neglect ||
(type == RouteInformationReportingType.none &&
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
_valueInEngine.location == routeInformation.location);
// GoRouteInformationParser should always report encoded route match list
// in the state.
assert(routeInformation.state != null);
final bool replace;
switch (type) {
case RouteInformationReportingType.none:
if (_valueInEngine.location == routeInformation.location &&
const DeepCollectionEquality()
.equals(_valueInEngine.state, routeInformation.state)) {
return;
}
replace = _valueInEngine == _kEmptyRouteInformation;
break;
case RouteInformationReportingType.neglect:
replace = true;
break;
case RouteInformationReportingType.navigate:
replace = false;
break;
}
SystemNavigator.selectMultiEntryHistory();
// TODO(chunhtai): report extra to browser through state if possible
// See https://github.com/flutter/flutter/issues/108142
SystemNavigator.routeInformationUpdated(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use, unnecessary_null_checks, unnecessary_non_null_assertion
// ignore: unnecessary_null_checks, unnecessary_non_null_assertion
location: routeInformation.location!,
state: routeInformation.state,
replace: replace,
);
_value = routeInformation;
_valueInEngine = routeInformation;
_value = _valueInEngine = routeInformation;
}
@override
RouteInformation get value => _value;
RouteInformation _value;
set value(RouteInformation other) {
void _setValue(String location, Object state) {
final bool shouldNotify =
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
_value.location != other.location || _value.state != other.state;
_value = other;
_value.location != location || _value.state != state;
_value = RouteInformation(location: location, state: state);
if (shouldNotify) {
notifyListeners();
}
}
RouteInformation _valueInEngine =
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
RouteInformation(location: _binding.platformDispatcher.defaultRouteName);
/// Pushes the `location` as a new route on top of `base`.
Future<T?> push<T>(String location,
{required RouteMatchList base, Object? extra}) {
final Completer<T?> completer = Completer<T?>();
_setValue(
location,
RouteInformationState<T>(
extra: extra,
baseRouteMatchList: base,
completer: completer,
type: NavigatingType.push,
),
);
return completer.future;
}
/// Replace the current route matches with the `location`.
void go(String location, {Object? extra}) {
_setValue(
location,
RouteInformationState<void>(
extra: extra,
type: NavigatingType.go,
),
);
}
/// Restores the current route matches with the `encodedMatchList`.
void restore(String location, {required Object encodedMatchList}) {
_setValue(
location,
encodedMatchList,
);
}
/// Removes the top-most route match from `base` and pushes the `location` as a
/// new route on top.
Future<T?> pushReplacement<T>(String location,
{required RouteMatchList base, Object? extra}) {
final Completer<T?> completer = Completer<T?>();
_setValue(
location,
RouteInformationState<T>(
extra: extra,
baseRouteMatchList: base,
completer: completer,
type: NavigatingType.pushReplacement,
),
);
return completer.future;
}
/// Replaces the top-most route match from `base` with the `location`.
Future<T?> replace<T>(String location,
{required RouteMatchList base, Object? extra}) {
final Completer<T?> completer = Completer<T?>();
_setValue(
location,
RouteInformationState<T>(
extra: extra,
baseRouteMatchList: base,
completer: completer,
type: NavigatingType.replace,
),
);
return completer.future;
}
RouteInformation _valueInEngine;
void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
if (_value == routeInformation) {
return;
}
_value = routeInformation;
_valueInEngine = routeInformation;
if (routeInformation.state != null) {
_value = _valueInEngine = routeInformation;
} else {
_value = RouteInformation(
location: routeInformation.location,
state: RouteInformationState<void>(type: NavigatingType.go),
);
_valueInEngine = _kEmptyRouteInformation;
}
notifyListeners();
}
@ -113,9 +263,6 @@ class GoRouteInformationProvider extends RouteInformationProvider
@override
Future<bool> didPushRoute(String route) {
assert(hasListeners);
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
_platformReportsNewRouteInformation(RouteInformation(location: route));
return SynchronousFuture<bool>(true);
}

View File

@ -3,12 +3,14 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'matching.dart';
import 'configuration.dart';
import 'path_utils.dart';
import 'route.dart';
/// An matched result by matching a [RouteBase] against a location.
///
@ -19,8 +21,6 @@ class RouteMatch {
const RouteMatch({
required this.route,
required this.matchedLocation,
required this.extra,
required this.error,
required this.pageKey,
});
@ -34,14 +34,11 @@ class RouteMatch {
required String remainingLocation, // e.g. person/p1
required String matchedLocation, // e.g. /family/f2
required Map<String, String> pathParameters,
required Object? extra,
}) {
if (route is ShellRouteBase) {
return RouteMatch(
route: route,
matchedLocation: remainingLocation,
extra: extra,
error: null,
pageKey: ValueKey<String>(route.hashCode.toString()),
);
} else if (route is GoRoute) {
@ -62,12 +59,11 @@ class RouteMatch {
return RouteMatch(
route: route,
matchedLocation: newMatchedLocation,
extra: extra,
error: null,
pageKey: ValueKey<String>(route.hashCode.toString()),
);
}
throw MatcherError('Unexpected route type: $route', remainingLocation);
assert(false, 'Unexpected route type: $route');
return null;
}
/// The matched route.
@ -83,12 +79,6 @@ class RouteMatch {
/// matchedLocation = '/family/f2'
final String matchedLocation;
/// An extra object to pass along with the navigation.
final Object? extra;
/// An exception if there was an error during matching.
final Exception? error;
/// Value key of type string, to hold a unique reference to a page.
final ValueKey<String> pageKey;
@ -100,44 +90,49 @@ class RouteMatch {
return other is RouteMatch &&
route == other.route &&
matchedLocation == other.matchedLocation &&
extra == other.extra &&
pageKey == other.pageKey;
}
@override
int get hashCode => Object.hash(route, matchedLocation, extra, pageKey);
int get hashCode => Object.hash(route, matchedLocation, pageKey);
}
/// The route match that represent route pushed through [GoRouter.push].
class ImperativeRouteMatch<T> extends RouteMatch {
class ImperativeRouteMatch extends RouteMatch {
/// Constructor for [ImperativeRouteMatch].
ImperativeRouteMatch({
required super.pageKey,
required this.matches,
}) : _completer = Completer<T?>(),
super(
route: matches.last.route,
matchedLocation: matches.last.matchedLocation,
extra: matches.last.extra,
error: matches.last.error,
ImperativeRouteMatch(
{required super.pageKey, required this.matches, required this.completer})
: super(
route: _getsLastRouteFromMatches(matches),
matchedLocation: _getsMatchedLocationFromMatches(matches),
);
static RouteBase _getsLastRouteFromMatches(RouteMatchList matchList) {
if (matchList.isError) {
return GoRoute(
path: 'error', builder: (_, __) => throw UnimplementedError());
}
return matchList.last.route;
}
static String _getsMatchedLocationFromMatches(RouteMatchList matchList) {
if (matchList.isError) {
return matchList.uri.toString();
}
return matchList.last.matchedLocation;
}
/// The matches that produces this route match.
final RouteMatchList matches;
/// The completer for the future returned by [GoRouter.push].
final Completer<T?> _completer;
final Completer<Object?> completer;
/// Called when the corresponding [Route] associated with this route match is
/// completed.
void complete([dynamic value]) {
_completer.complete(value as T?);
completer.complete(value);
}
/// The future of the [RouteMatch] completer.
/// When the future completes, this will return the value passed to [complete].
Future<T?> get future => _completer.future;
// An ImperativeRouteMatch has its own life cycle due the the _completer.
// comparing _completer between instances would be the same thing as
// comparing object reference.
@ -149,3 +144,304 @@ class ImperativeRouteMatch<T> extends RouteMatch {
@override
int get hashCode => identityHashCode(this);
}
/// The list of [RouteMatch] objects.
///
/// This corresponds to the GoRouter's history.
@immutable
class RouteMatchList {
/// RouteMatchList constructor.
RouteMatchList({
required this.matches,
required this.uri,
this.extra,
this.error,
required this.pathParameters,
}) : fullPath = _generateFullPath(matches);
/// Constructs an empty matches object.
static RouteMatchList empty = RouteMatchList(
matches: const <RouteMatch>[],
uri: Uri(),
pathParameters: const <String, String>{});
/// The route matches.
final List<RouteMatch> matches;
/// Parameters for the matched route, URI-encoded.
///
/// The parameters only reflects [RouteMatch]s that are not
/// [ImperativeRouteMatch].
final Map<String, String> pathParameters;
/// The uri of the current match.
///
/// This uri only reflects [RouteMatch]s that are not [ImperativeRouteMatch].
final Uri uri;
/// An extra object to pass along with the navigation.
final Object? extra;
/// An exception if there was an error during matching.
final Exception? error;
/// the full path pattern that matches the uri.
///
/// For example:
///
/// ```dart
/// '/family/:fid/person/:pid'
/// ```
final String fullPath;
/// Generates the full path (ex: `'/family/:fid/person/:pid'`) of a list of
/// [RouteMatch].
///
/// This method ignores [ImperativeRouteMatch]s in the `matches`, as they
/// don't contribute to the path.
///
/// This methods considers that [matches]'s elements verify the go route
/// structure given to `GoRouter`. For example, if the routes structure is
///
/// ```dart
/// GoRoute(
/// path: '/a',
/// routes: [
/// GoRoute(
/// path: 'b',
/// routes: [
/// GoRoute(
/// path: 'c',
/// ),
/// ],
/// ),
/// ],
/// ),
/// ```
///
/// The [matches] must be the in same order of how GoRoutes are matched.
///
/// ```dart
/// [RouteMatchA(), RouteMatchB(), RouteMatchC()]
/// ```
static String _generateFullPath(Iterable<RouteMatch> matches) {
final StringBuffer buffer = StringBuffer();
bool addsSlash = false;
for (final RouteMatch match in matches
.where((RouteMatch match) => match is! ImperativeRouteMatch)) {
final RouteBase route = match.route;
if (route is GoRoute) {
if (addsSlash) {
buffer.write('/');
}
buffer.write(route.path);
addsSlash = addsSlash || route.path != '/';
}
}
return buffer.toString();
}
/// Returns true if there are no matches.
bool get isEmpty => matches.isEmpty;
/// Returns true if there are matches.
bool get isNotEmpty => matches.isNotEmpty;
/// Returns a new instance of RouteMatchList with the input `match` pushed
/// onto the current instance.
RouteMatchList push(ImperativeRouteMatch match) {
// Imperative route match doesn't change the uri and path parameters.
return _copyWith(matches: <RouteMatch>[...matches, match]);
}
/// Returns a new instance of RouteMatchList with the input `match` removed
/// from the current instance.
RouteMatchList remove(RouteMatch match) {
final List<RouteMatch> newMatches = matches.toList();
final int index = newMatches.indexOf(match);
assert(index != -1);
newMatches.removeRange(index, newMatches.length);
// Also pop ShellRoutes when there are no subsequent route matches
while (newMatches.isNotEmpty && newMatches.last.route is ShellRouteBase) {
newMatches.removeLast();
}
// Removing ImperativeRouteMatch should not change uri and pathParameters.
if (match is ImperativeRouteMatch) {
return _copyWith(matches: newMatches);
}
final String fullPath = _generateFullPath(
newMatches.where((RouteMatch match) => match is! ImperativeRouteMatch));
// Need to remove path parameters that are no longer in the fullPath.
final List<String> newParameters = <String>[];
patternToRegExp(fullPath, newParameters);
final Set<String> validParameters = newParameters.toSet();
final Map<String, String> newPathParameters =
Map<String, String>.fromEntries(
pathParameters.entries.where((MapEntry<String, String> value) =>
validParameters.contains(value.key)),
);
final Uri newUri =
uri.replace(path: patternToPath(fullPath, newPathParameters));
return _copyWith(
matches: newMatches,
uri: newUri,
pathParameters: newPathParameters,
);
}
/// The last matching route.
RouteMatch get last => matches.last;
/// Returns true if the current match intends to display an error screen.
bool get isError => error != null;
/// The routes for each of the matches.
List<RouteBase> get routes => matches.map((RouteMatch e) => e.route).toList();
RouteMatchList _copyWith({
List<RouteMatch>? matches,
Uri? uri,
Map<String, String>? pathParameters,
}) {
return RouteMatchList(
matches: matches ?? this.matches,
uri: uri ?? this.uri,
extra: extra,
error: error,
pathParameters: pathParameters ?? this.pathParameters);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is RouteMatchList &&
uri == other.uri &&
extra == other.extra &&
error == other.error &&
const ListEquality<RouteMatch>().equals(matches, other.matches) &&
const MapEquality<String, String>()
.equals(pathParameters, other.pathParameters);
}
@override
int get hashCode {
return Object.hash(
Object.hashAll(matches),
uri,
extra,
error,
Object.hashAllUnordered(
pathParameters.entries.map<int>((MapEntry<String, String> entry) =>
Object.hash(entry.key, entry.value)),
),
);
}
@override
String toString() {
return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)';
}
}
/// Handles encoding and decoding of [RouteMatchList] objects to a format
/// suitable for using with [StandardMessageCodec].
///
/// The primary use of this class is for state restoration.
class RouteMatchListCodec extends Codec<RouteMatchList, Map<Object?, Object?>> {
/// Creates a new [RouteMatchListCodec] object.
RouteMatchListCodec(RouteConfiguration configuration)
: decoder = _RouteMatchListDecoder(configuration);
static const String _locationKey = 'location';
static const String _extraKey = 'state';
static const String _imperativeMatchesKey = 'imperativeMatches';
static const String _pageKey = 'pageKey';
@override
final Converter<RouteMatchList, Map<Object?, Object?>> encoder =
const _RouteMatchListEncoder();
@override
final Converter<Map<Object?, Object?>, RouteMatchList> decoder;
}
class _RouteMatchListEncoder
extends Converter<RouteMatchList, Map<Object?, Object?>> {
const _RouteMatchListEncoder();
@override
Map<Object?, Object?> convert(RouteMatchList input) {
final List<Map<Object?, Object?>> imperativeMatches = input.matches
.whereType<ImperativeRouteMatch>()
.map((ImperativeRouteMatch e) => _toPrimitives(
e.matches.uri.toString(), e.matches.extra,
pageKey: e.pageKey.value))
.toList();
return _toPrimitives(input.uri.toString(), input.extra,
imperativeMatches: imperativeMatches);
}
static Map<Object?, Object?> _toPrimitives(String location, Object? extra,
{List<Map<Object?, Object?>>? imperativeMatches, String? pageKey}) {
String? encodedExtra;
try {
encodedExtra = json.encoder.convert(extra);
} on JsonUnsupportedObjectError {/* give up if not serializable */}
return <Object?, Object?>{
RouteMatchListCodec._locationKey: location,
if (encodedExtra != null) RouteMatchListCodec._extraKey: encodedExtra,
if (imperativeMatches != null)
RouteMatchListCodec._imperativeMatchesKey: imperativeMatches,
if (pageKey != null) RouteMatchListCodec._pageKey: pageKey,
};
}
}
class _RouteMatchListDecoder
extends Converter<Map<Object?, Object?>, RouteMatchList> {
_RouteMatchListDecoder(this.configuration);
final RouteConfiguration configuration;
@override
RouteMatchList convert(Map<Object?, Object?> input) {
final String rootLocation =
input[RouteMatchListCodec._locationKey]! as String;
final String? encodedExtra =
input[RouteMatchListCodec._extraKey] as String?;
final Object? extra;
if (encodedExtra != null) {
extra = json.decoder.convert(encodedExtra);
} else {
extra = null;
}
RouteMatchList matchList =
configuration.findMatch(rootLocation, extra: extra);
final List<Object?>? imperativeMatches =
input[RouteMatchListCodec._imperativeMatchesKey] as List<Object?>?;
if (imperativeMatches != null) {
for (final Map<Object?, Object?> encodedImperativeMatch
in imperativeMatches.whereType<Map<Object?, Object?>>()) {
final RouteMatchList imperativeMatchList =
convert(encodedImperativeMatch);
final ValueKey<String> pageKey = ValueKey<String>(
encodedImperativeMatch[RouteMatchListCodec._pageKey]! as String);
final ImperativeRouteMatch imperativeMatch = ImperativeRouteMatch(
pageKey: pageKey,
// TODO(chunhtai): Figure out a way to preserve future.
// https://github.com/flutter/flutter/issues/128122.
completer: Completer<Object?>(),
matches: imperativeMatchList,
);
matchList = matchList.push(imperativeMatch);
}
}
return matchList;
}
}

View File

@ -1,479 +0,0 @@
// 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:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'match.dart';
import 'path_utils.dart';
/// Converts a location into a list of [RouteMatch] objects.
class RouteMatcher {
/// [RouteMatcher] constructor.
RouteMatcher(this.configuration);
/// The route configuration.
final RouteConfiguration configuration;
/// Finds the routes that matched the given URL.
RouteMatchList findMatch(String location, {Object? extra}) {
final Uri uri = Uri.parse(canonicalUri(location));
final Map<String, String> pathParameters = <String, String>{};
final List<RouteMatch> matches =
_getLocRouteMatches(uri, extra, pathParameters);
return RouteMatchList(
matches: matches, uri: uri, pathParameters: pathParameters);
}
List<RouteMatch> _getLocRouteMatches(
Uri uri, Object? extra, Map<String, String> pathParameters) {
final List<RouteMatch>? result = _getLocRouteRecursively(
location: uri.path,
remainingLocation: uri.path,
routes: configuration.routes,
matchedLocation: '',
pathParameters: pathParameters,
extra: extra,
);
if (result == null) {
throw MatcherError('no routes for location', uri.toString());
}
return result;
}
}
/// The list of [RouteMatch] objects.
///
/// This corresponds to the GoRouter's history.
@immutable
class RouteMatchList {
/// RouteMatchList constructor.
RouteMatchList({
required this.matches,
required this.uri,
required this.pathParameters,
}) : fullPath = _generateFullPath(matches);
/// Constructs an empty matches object.
static RouteMatchList empty = RouteMatchList(
matches: const <RouteMatch>[],
uri: Uri(),
pathParameters: const <String, String>{});
/// The route matches.
final List<RouteMatch> matches;
/// Parameters for the matched route, URI-encoded.
///
/// The parameters only reflects [RouteMatch]s that are not
/// [ImperativeRouteMatch].
final Map<String, String> pathParameters;
/// The uri of the current match.
///
/// This uri only reflects [RouteMatch]s that are not [ImperativeRouteMatch].
final Uri uri;
/// the full path pattern that matches the uri.
///
/// For example:
///
/// ```dart
/// '/family/:fid/person/:pid'
/// ```
final String fullPath;
/// Generates the full path (ex: `'/family/:fid/person/:pid'`) of a list of
/// [RouteMatch].
///
/// This method ignores [ImperativeRouteMatch]s in the `matches`, as they
/// don't contribute to the path.
///
/// This methods considers that [matches]'s elements verify the go route
/// structure given to `GoRouter`. For example, if the routes structure is
///
/// ```dart
/// GoRoute(
/// path: '/a',
/// routes: [
/// GoRoute(
/// path: 'b',
/// routes: [
/// GoRoute(
/// path: 'c',
/// ),
/// ],
/// ),
/// ],
/// ),
/// ```
///
/// The [matches] must be the in same order of how GoRoutes are matched.
///
/// ```dart
/// [RouteMatchA(), RouteMatchB(), RouteMatchC()]
/// ```
static String _generateFullPath(Iterable<RouteMatch> matches) {
final StringBuffer buffer = StringBuffer();
bool addsSlash = false;
for (final RouteMatch match in matches
.where((RouteMatch match) => match is! ImperativeRouteMatch)) {
final RouteBase route = match.route;
if (route is GoRoute) {
if (addsSlash) {
buffer.write('/');
}
buffer.write(route.path);
addsSlash = addsSlash || route.path != '/';
}
}
return buffer.toString();
}
/// Returns true if there are no matches.
bool get isEmpty => matches.isEmpty;
/// Returns true if there are matches.
bool get isNotEmpty => matches.isNotEmpty;
/// Returns a new instance of RouteMatchList with the input `match` pushed
/// onto the current instance.
RouteMatchList push<T>(ImperativeRouteMatch<T> match) {
// Imperative route match doesn't change the uri and path parameters.
return _copyWith(matches: <RouteMatch>[...matches, match]);
}
/// Returns a new instance of RouteMatchList with the input `match` removed
/// from the current instance.
RouteMatchList remove(RouteMatch match) {
final List<RouteMatch> newMatches = matches.toList();
final int index = newMatches.indexOf(match);
assert(index != -1);
newMatches.removeRange(index, newMatches.length);
// Also pop ShellRoutes when there are no subsequent route matches
while (newMatches.isNotEmpty && newMatches.last.route is ShellRouteBase) {
newMatches.removeLast();
}
// Removing ImperativeRouteMatch should not change uri and pathParameters.
if (match is ImperativeRouteMatch) {
return _copyWith(matches: newMatches);
}
final String fullPath = _generateFullPath(
newMatches.where((RouteMatch match) => match is! ImperativeRouteMatch));
// Need to remove path parameters that are no longer in the fullPath.
final List<String> newParameters = <String>[];
patternToRegExp(fullPath, newParameters);
final Set<String> validParameters = newParameters.toSet();
final Map<String, String> newPathParameters =
Map<String, String>.fromEntries(
pathParameters.entries.where((MapEntry<String, String> value) =>
validParameters.contains(value.key)),
);
final Uri newUri =
uri.replace(path: patternToPath(fullPath, newPathParameters));
return _copyWith(
matches: newMatches,
uri: newUri,
pathParameters: newPathParameters,
);
}
/// An optional object provided by the app during navigation.
Object? get extra => matches.isEmpty ? null : matches.last.extra;
/// The last matching route.
RouteMatch get last => matches.last;
/// Returns true if the current match intends to display an error screen.
bool get isError => error != null;
/// Returns the error that this match intends to display.
Exception? get error => matches.firstOrNull?.error;
/// The routes for each of the matches.
List<RouteBase> get routes => matches.map((RouteMatch e) => e.route).toList();
RouteMatchList _copyWith({
List<RouteMatch>? matches,
Uri? uri,
Map<String, String>? pathParameters,
}) {
return RouteMatchList(
matches: matches ?? this.matches,
uri: uri ?? this.uri,
pathParameters: pathParameters ?? this.pathParameters);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is RouteMatchList &&
const ListEquality<RouteMatch>().equals(matches, other.matches) &&
uri == other.uri &&
const MapEquality<String, String>()
.equals(pathParameters, other.pathParameters);
}
@override
int get hashCode {
return Object.hash(
Object.hashAll(matches),
uri,
Object.hashAllUnordered(
pathParameters.entries.map<int>((MapEntry<String, String> entry) =>
Object.hash(entry.key, entry.value)),
),
);
}
@override
String toString() {
return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)';
}
/// Returns a pre-parsed [RouteInformation], containing a reference to this
/// match list.
RouteInformation toPreParsedRouteInformation() {
return RouteInformation(
// TODO(tolo): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
location: uri.toString(),
state: this,
);
}
/// Attempts to extract a pre-parsed match list from the provided
/// [RouteInformation].
static RouteMatchList? fromPreParsedRouteInformation(
RouteInformation routeInformation) {
if (routeInformation.state is RouteMatchList) {
return routeInformation.state! as RouteMatchList;
}
return null;
}
}
/// Handles encoding and decoding of [RouteMatchList] objects to a format
/// suitable for using with [StandardMessageCodec].
///
/// The primary use of this class is for state restoration.
class RouteMatchListCodec {
/// Creates a new [RouteMatchListCodec] object.
RouteMatchListCodec(this._matcher);
static const String _encodedDataKey = 'go_router/encoded_route_match_list';
static const String _locationKey = 'location';
static const String _stateKey = 'state';
static const String _imperativeMatchesKey = 'imperativeMatches';
static const String _pageKey = 'pageKey';
final RouteMatcher _matcher;
/// Encodes the provided [RouteMatchList].
Object? encodeMatchList(RouteMatchList matchlist) {
if (matchlist.isEmpty) {
return null;
}
final List<Map<Object?, Object?>> imperativeMatches = matchlist.matches
.whereType<ImperativeRouteMatch<Object?>>()
.map((ImperativeRouteMatch<Object?> e) => _toPrimitives(
e.matches.uri.toString(), e.extra,
pageKey: e.pageKey.value))
.toList();
return <Object?, Object?>{
_encodedDataKey: _toPrimitives(
matchlist.uri.toString(), matchlist.matches.first.extra,
imperativeMatches: imperativeMatches),
};
}
static Map<Object?, Object?> _toPrimitives(String location, Object? state,
{List<dynamic>? imperativeMatches, String? pageKey}) {
return <Object?, Object?>{
_locationKey: location,
_stateKey: state,
if (imperativeMatches != null) _imperativeMatchesKey: imperativeMatches,
if (pageKey != null) _pageKey: pageKey,
};
}
/// Attempts to decode the provided object into a [RouteMatchList].
RouteMatchList? decodeMatchList(Object? object) {
if (object is Map && object[_encodedDataKey] is Map) {
final Map<Object?, Object?> data =
object[_encodedDataKey] as Map<Object?, Object?>;
final Object? rootLocation = data[_locationKey];
if (rootLocation is! String) {
return null;
}
final RouteMatchList matchList =
_matcher.findMatch(rootLocation, extra: data[_stateKey]);
final List<Object?>? imperativeMatches =
data[_imperativeMatchesKey] as List<Object?>?;
if (imperativeMatches != null) {
for (int i = 0; i < imperativeMatches.length; i++) {
final Object? match = imperativeMatches[i];
if (match is! Map ||
match[_locationKey] is! String ||
match[_pageKey] is! String) {
continue;
}
final ValueKey<String> pageKey =
ValueKey<String>(match[_pageKey] as String);
final RouteMatchList imperativeMatchList = _matcher.findMatch(
match[_locationKey] as String,
extra: match[_stateKey]);
final ImperativeRouteMatch<Object?> imperativeMatch =
ImperativeRouteMatch<Object?>(
pageKey: pageKey,
matches: imperativeMatchList,
);
matchList.push(imperativeMatch);
}
}
return matchList;
}
return null;
}
}
/// An error that occurred during matching.
class MatcherError extends Error {
/// Constructs a [MatcherError].
MatcherError(String message, this.location) : message = '$message: $location';
/// The error message.
final String message;
/// The location that failed to match.
final String location;
@override
String toString() {
return message;
}
}
/// Returns the list of `RouteMatch` corresponding to the given `loc`.
///
/// For example, for a given `loc` `/a/b/c/d`, this function will return the
/// list of [RouteBase] `[GoRouteA(), GoRouterB(), GoRouteC(), GoRouterD()]`.
///
/// - [location] is the complete URL to match (without the query parameters). For
/// example, for the URL `/a/b?c=0`, [location] will be `/a/b`.
/// - [remainingLocation] is the remaining part of the URL to match while [matchedLocation]
/// is the part of the URL that has already been matched. For examples, for
/// the URL `/a/b/c/d`, at some point, [remainingLocation] would be `/c/d` and
/// [matchedLocation] will be `/a/b`.
/// - [routes] are the possible [RouteBase] to match to [remainingLocation].
List<RouteMatch>? _getLocRouteRecursively({
required String location,
required String remainingLocation,
required String matchedLocation,
required List<RouteBase> routes,
required Map<String, String> pathParameters,
required Object? extra,
}) {
List<RouteMatch>? result;
late Map<String, String> subPathParameters;
// find the set of matches at this level of the tree
for (final RouteBase route in routes) {
subPathParameters = <String, String>{};
final RouteMatch? match = RouteMatch.match(
route: route,
remainingLocation: remainingLocation,
matchedLocation: matchedLocation,
pathParameters: subPathParameters,
extra: extra,
);
if (match == null) {
continue;
}
if (match.route is GoRoute &&
match.matchedLocation.toLowerCase() == location.toLowerCase()) {
// If it is a complete match, then return the matched route
// NOTE: need a lower case match because matchedLocation is canonicalized to match
// the path case whereas the location can be of any case and still match
result = <RouteMatch>[match];
} else if (route.routes.isEmpty) {
// If it is partial match but no sub-routes, bail.
continue;
} else {
// Otherwise, recurse
final String childRestLoc;
final String newParentSubLoc;
if (match.route is ShellRouteBase) {
childRestLoc = remainingLocation;
newParentSubLoc = matchedLocation;
} else {
assert(location.startsWith(match.matchedLocation));
assert(remainingLocation.isNotEmpty);
childRestLoc = location.substring(match.matchedLocation.length +
(match.matchedLocation == '/' ? 0 : 1));
newParentSubLoc = match.matchedLocation;
}
final List<RouteMatch>? subRouteMatch = _getLocRouteRecursively(
location: location,
remainingLocation: childRestLoc,
matchedLocation: newParentSubLoc,
routes: route.routes,
pathParameters: subPathParameters,
extra: extra,
);
// If there's no sub-route matches, there is no match for this location
if (subRouteMatch == null) {
continue;
}
result = <RouteMatch>[match, ...subRouteMatch];
}
// Should only reach here if there is a match.
break;
}
if (result != null) {
pathParameters.addAll(subPathParameters);
}
return result;
}
/// The match used when there is an error during parsing.
RouteMatchList errorScreen(Uri uri, String errorMessage) {
final Exception error = Exception(errorMessage);
return RouteMatchList(
matches: <RouteMatch>[
RouteMatch(
matchedLocation: uri.path,
extra: null,
error: error,
route: GoRoute(
path: uri.toString(),
pageBuilder: (BuildContext context, GoRouterState state) {
throw UnimplementedError();
},
),
pageKey: const ValueKey<String>('error'),
),
],
uri: uri,
pathParameters: const <String, String>{},
);
}

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../match.dart';
/// Thrown when [GoRouter] is used incorrectly.
class GoError extends Error {
/// Constructs a [GoError]
@ -13,3 +15,24 @@ class GoError extends Error {
@override
String toString() => 'GoError: $message';
}
/// A configuration error detected while processing redirects.
class RedirectionError extends Error implements UnsupportedError {
/// RedirectionError constructor.
RedirectionError(this.message, this.matches, this.location);
/// The matches that were found while processing redirects.
final List<RouteMatchList> matches;
@override
final String message;
/// The location that was originally navigated to, before redirection began.
final Uri location;
@override
String toString() => '${super.toString()} ${<String>[
...matches
.map((RouteMatchList routeMatches) => routeMatches.uri.toString()),
].join(' => ')}';
}

View File

@ -3,17 +3,16 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import '../go_router.dart';
import 'configuration.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
import 'path_utils.dart';
import 'redirection.dart';
/// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher].
/// Also performs redirection using [RouteRedirector].
@ -21,27 +20,14 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
/// Creates a [GoRouteInformationParser].
GoRouteInformationParser({
required this.configuration,
this.debugRequireGoRouteInformationProvider = false,
}) : matcher = RouteMatcher(configuration),
redirector = redirect;
}) : _routeMatchListCodec = RouteMatchListCodec(configuration);
/// The route configuration for the app.
/// The route configuration used for parsing [RouteInformation]s.
final RouteConfiguration configuration;
/// The route matcher.
final RouteMatcher matcher;
final RouteMatchListCodec _routeMatchListCodec;
/// The route redirector.
final RouteRedirector redirector;
/// A debug property to assert [GoRouteInformationProvider] is in use along
/// with this parser.
///
/// An assertion error will be thrown if this property set to true and the
/// [GoRouteInformationProvider] is not in use.
///
/// Defaults to false.
final bool debugRequireGoRouteInformationProvider;
final Random _random = Random();
/// The future of current route parsing.
///
@ -55,64 +41,42 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
RouteInformation routeInformation,
BuildContext context,
) {
assert(routeInformation.state != null);
final Object state = routeInformation.state!;
if (state is! RouteInformationState) {
// This is a result of browser backward/forward button or state
// restoration. In this case, the route match list is already stored in
// the state.
final RouteMatchList matchList =
_routeMatchListCodec.decode(state as Map<Object?, Object?>);
return debugParserFuture = _redirect(context, matchList);
}
late final RouteMatchList initialMatches;
try {
final RouteMatchList? preParsedMatchList =
RouteMatchList.fromPreParsedRouteInformation(routeInformation);
if (preParsedMatchList != null) {
initialMatches = preParsedMatchList;
} else {
initialMatches =
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use, unnecessary_non_null_assertion
initialMatches = matcher.findMatch(routeInformation.location!,
extra: routeInformation.state);
}
} on MatcherError {
configuration.findMatch(routeInformation.location!, extra: state.extra);
if (initialMatches.isError) {
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
log.info('No initial matches: ${routeInformation.location}');
// If there is a matching error for the initial location, we should
// still try to process the top-level redirects.
initialMatches = RouteMatchList(
matches: const <RouteMatch>[],
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use, unnecessary_non_null_assertion
uri: Uri.parse(canonicalUri(routeInformation.location!)),
pathParameters: const <String, String>{},
);
}
Future<RouteMatchList> processRedirectorResult(RouteMatchList matches) {
if (matches.isEmpty) {
return SynchronousFuture<RouteMatchList>(errorScreen(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use, unnecessary_non_null_assertion
Uri.parse(routeInformation.location!),
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use, unnecessary_non_null_assertion
MatcherError('no routes for location', routeInformation.location!)
.toString()));
}
return SynchronousFuture<RouteMatchList>(matches);
}
final FutureOr<RouteMatchList> redirectorResult = redirector(
return debugParserFuture = _redirect(
context,
SynchronousFuture<RouteMatchList>(initialMatches),
configuration,
matcher,
extra: routeInformation.state,
);
if (redirectorResult is RouteMatchList) {
return processRedirectorResult(redirectorResult);
}
return debugParserFuture = redirectorResult.then(processRedirectorResult);
initialMatches,
).then<RouteMatchList>((RouteMatchList matchList) {
return _updateRouteMatchList(
matchList,
baseRouteMatchList: state.baseRouteMatchList,
completer: state.completer,
type: state.type,
);
});
}
@override
@ -128,16 +92,70 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
if (configuration.isEmpty) {
return null;
}
if (configuration.matches.last is ImperativeRouteMatch) {
if (GoRouter.optionURLReflectsImperativeAPIs &&
configuration.matches.last is ImperativeRouteMatch) {
configuration =
(configuration.matches.last as ImperativeRouteMatch<Object?>).matches;
(configuration.matches.last as ImperativeRouteMatch).matches;
}
return RouteInformation(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
location: configuration.uri.toString(),
state: configuration.extra,
state: _routeMatchListCodec.encode(configuration),
);
}
Future<RouteMatchList> _redirect(
BuildContext context, RouteMatchList routeMatch) {
final FutureOr<RouteMatchList> redirectedFuture = configuration
.redirect(context, routeMatch, redirectHistory: <RouteMatchList>[]);
if (redirectedFuture is RouteMatchList) {
return SynchronousFuture<RouteMatchList>(redirectedFuture);
}
return redirectedFuture;
}
RouteMatchList _updateRouteMatchList(
RouteMatchList newMatchList, {
required RouteMatchList? baseRouteMatchList,
required Completer<Object?>? completer,
required NavigatingType type,
}) {
switch (type) {
case NavigatingType.push:
return baseRouteMatchList!.push(
ImperativeRouteMatch(
pageKey: _getUniqueValueKey(),
completer: completer!,
matches: newMatchList,
),
);
case NavigatingType.pushReplacement:
final RouteMatch routeMatch = baseRouteMatchList!.last;
return baseRouteMatchList.remove(routeMatch).push(
ImperativeRouteMatch(
pageKey: _getUniqueValueKey(),
completer: completer!,
matches: newMatchList,
),
);
case NavigatingType.replace:
final RouteMatch routeMatch = baseRouteMatchList!.last;
return baseRouteMatchList.remove(routeMatch).push(
ImperativeRouteMatch(
pageKey: routeMatch.pageKey,
completer: completer!,
matches: newMatchList,
),
);
case NavigatingType.go:
return newMatchList;
}
}
ValueKey<String> _getUniqueValueKey() {
return ValueKey<String>(String.fromCharCodes(
List<int>.generate(32, (_) => _random.nextInt(33) + 89)));
}
}

View File

@ -1,237 +0,0 @@
// 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 'dart:async';
import 'package:flutter/cupertino.dart';
import 'configuration.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
/// A GoRouter redirector function.
typedef RouteRedirector = FutureOr<RouteMatchList> Function(
BuildContext, FutureOr<RouteMatchList>, RouteConfiguration, RouteMatcher,
{List<RouteMatchList>? redirectHistory, Object? extra});
/// Processes redirects by returning a new [RouteMatchList] representing the new
/// location.
FutureOr<RouteMatchList> redirect(
BuildContext context,
FutureOr<RouteMatchList> prevMatchListFuture,
RouteConfiguration configuration,
RouteMatcher matcher,
{List<RouteMatchList>? redirectHistory,
Object? extra}) {
FutureOr<RouteMatchList> processRedirect(RouteMatchList prevMatchList) {
final String prevLocation = prevMatchList.uri.toString();
FutureOr<RouteMatchList> processTopLevelRedirect(
String? topRedirectLocation) {
if (topRedirectLocation != null && topRedirectLocation != prevLocation) {
final RouteMatchList newMatch = _getNewMatches(
topRedirectLocation,
prevMatchList.uri,
configuration,
matcher,
redirectHistory!,
);
if (newMatch.isError) {
return newMatch;
}
return redirect(
context,
newMatch,
configuration,
matcher,
redirectHistory: redirectHistory,
extra: extra,
);
}
FutureOr<RouteMatchList> processRouteLevelRedirect(
String? routeRedirectLocation) {
if (routeRedirectLocation != null &&
routeRedirectLocation != prevLocation) {
final RouteMatchList newMatch = _getNewMatches(
routeRedirectLocation,
prevMatchList.uri,
configuration,
matcher,
redirectHistory!,
);
if (newMatch.isError) {
return newMatch;
}
return redirect(
context,
newMatch,
configuration,
matcher,
redirectHistory: redirectHistory,
extra: extra,
);
}
return prevMatchList;
}
final FutureOr<String?> routeLevelRedirectResult =
_getRouteLevelRedirect(context, configuration, prevMatchList, 0);
if (routeLevelRedirectResult is String?) {
return processRouteLevelRedirect(routeLevelRedirectResult);
}
return routeLevelRedirectResult
.then<RouteMatchList>(processRouteLevelRedirect);
}
redirectHistory ??= <RouteMatchList>[prevMatchList];
// Check for top-level redirect
final FutureOr<String?> topRedirectResult = configuration.topRedirect(
context,
GoRouterState(
configuration,
location: prevLocation,
name: null,
// No name available at the top level trim the query params off the
// sub-location to match route.redirect
matchedLocation: prevMatchList.uri.path,
queryParameters: prevMatchList.uri.queryParameters,
queryParametersAll: prevMatchList.uri.queryParametersAll,
extra: extra,
pageKey: const ValueKey<String>('topLevel'),
),
);
if (topRedirectResult is String?) {
return processTopLevelRedirect(topRedirectResult);
}
return topRedirectResult.then<RouteMatchList>(processTopLevelRedirect);
}
if (prevMatchListFuture is RouteMatchList) {
return processRedirect(prevMatchListFuture);
}
return prevMatchListFuture.then<RouteMatchList>(processRedirect);
}
FutureOr<String?> _getRouteLevelRedirect(
BuildContext context,
RouteConfiguration configuration,
RouteMatchList matchList,
int currentCheckIndex,
) {
if (currentCheckIndex >= matchList.matches.length) {
return null;
}
final RouteMatch match = matchList.matches[currentCheckIndex];
FutureOr<String?> processRouteRedirect(String? newLocation) =>
newLocation ??
_getRouteLevelRedirect(
context, configuration, matchList, currentCheckIndex + 1);
final RouteBase route = match.route;
FutureOr<String?> routeRedirectResult;
if (route is GoRoute && route.redirect != null) {
routeRedirectResult = route.redirect!(
context,
GoRouterState(
configuration,
location: matchList.uri.toString(),
matchedLocation: match.matchedLocation,
name: route.name,
path: route.path,
fullPath: matchList.fullPath,
extra: match.extra,
pathParameters: matchList.pathParameters,
queryParameters: matchList.uri.queryParameters,
queryParametersAll: matchList.uri.queryParametersAll,
pageKey: match.pageKey,
),
);
}
if (routeRedirectResult is String?) {
return processRouteRedirect(routeRedirectResult);
}
return routeRedirectResult.then<String?>(processRouteRedirect);
}
RouteMatchList _getNewMatches(
String newLocation,
Uri previousLocation,
RouteConfiguration configuration,
RouteMatcher matcher,
List<RouteMatchList> redirectHistory,
) {
try {
final RouteMatchList newMatch = matcher.findMatch(newLocation);
_addRedirect(redirectHistory, newMatch, previousLocation,
configuration.redirectLimit);
return newMatch;
} on RedirectionError catch (e) {
return _handleRedirectionError(e);
} on MatcherError catch (e) {
return _handleMatcherError(e);
}
}
RouteMatchList _handleMatcherError(MatcherError error) {
// The RouteRedirector uses the matcher to find the match, so a match
// exception can happen during redirection. For example, the redirector
// redirects from `/a` to `/b`, it needs to get the matches for `/b`.
log.info('Match error: ${error.message}');
final Uri uri = Uri.parse(error.location);
return errorScreen(uri, error.message);
}
RouteMatchList _handleRedirectionError(RedirectionError error) {
log.info('Redirection error: ${error.message}');
final Uri uri = error.location;
return errorScreen(uri, error.message);
}
/// A configuration error detected while processing redirects.
class RedirectionError extends Error implements UnsupportedError {
/// RedirectionError constructor.
RedirectionError(this.message, this.matches, this.location);
/// The matches that were found while processing redirects.
final List<RouteMatchList> matches;
@override
final String message;
/// The location that was originally navigated to, before redirection began.
final Uri location;
@override
String toString() => '${super.toString()} ${<String>[
...matches
.map((RouteMatchList routeMatches) => routeMatches.uri.toString()),
].join(' => ')}';
}
/// Adds the redirect to [redirects] if it is valid.
void _addRedirect(List<RouteMatchList> redirects, RouteMatchList newMatch,
Uri prevLocation, int redirectLimit) {
// Verify that the redirect can be parsed and is not already
// in the list of redirects
assert(() {
if (redirects.contains(newMatch)) {
throw RedirectionError('redirect loop detected',
<RouteMatchList>[...redirects, newMatch], prevLocation);
}
if (redirects.length > redirectLimit) {
throw RedirectionError('too many redirects',
<RouteMatchList>[...redirects, newMatch], prevLocation);
}
return true;
}());
redirects.add(newMatch);
assert(() {
log.info('redirecting to $newMatch');
return true;
}());
}

View File

@ -7,8 +7,8 @@ import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import '../go_router.dart';
import 'configuration.dart';
import 'match.dart';
import 'matching.dart';
import 'path_utils.dart';
import 'typedefs.dart';
@ -928,6 +928,8 @@ class StatefulNavigationShell extends StatefulWidget {
/// current [StatefulShellBranch].
final ShellRouteContext shellRouteContext;
final GoRouter _router;
/// The builder for a custom container for shell route Navigators.
final ShellNavigationContainerBuilder containerBuilder;
@ -936,8 +938,6 @@ class StatefulNavigationShell extends StatefulWidget {
/// Corresponds to the index in the branches field of [StatefulShellRoute].
final int currentIndex;
final GoRouter _router;
/// The associated [StatefulShellRoute].
StatefulShellRoute get route => shellRouteContext.route as StatefulShellRoute;
@ -948,6 +948,8 @@ class StatefulNavigationShell extends StatefulWidget {
/// [StatefulShellRoute]. If the branch has not been visited before, or if
/// initialLocation is true, this method will navigate to initial location of
/// the branch (see [StatefulShellBranch.initialLocation]).
// TODO(chunhtai): figure out a way to avoid putting navigation API in widget
// class.
void goBranch(int index, {bool initialLocation = false}) {
final StatefulShellRoute route =
shellRouteContext.route as StatefulShellRoute;
@ -977,7 +979,7 @@ class StatefulNavigationShell extends StatefulWidget {
/// Recursively traverses the routes of the provided StackedShellBranch to
/// find the first GoRoute, from which a full path will be derived.
final GoRoute route = branch.defaultRoute!;
return _router.locationForRoute(route)!;
return _router.configuration.locationForRoute(route)!;
}
}
@ -1019,7 +1021,6 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
StatefulShellRoute get route => widget.route;
GoRouter get _router => widget._router;
RouteMatcher get _matcher => _router.routeInformationParser.matcher;
final Map<StatefulShellBranch, _RestorableRouteMatchList> _branchLocations =
<StatefulShellBranch, _RestorableRouteMatchList>{};
@ -1040,7 +1041,7 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
[bool register = true]) {
return _branchLocations.putIfAbsent(branch, () {
final _RestorableRouteMatchList branchLocation =
_RestorableRouteMatchList(_matcher);
_RestorableRouteMatchList(_router.configuration);
if (register) {
registerForRestoration(
branchLocation, _branchLocationRestorationScopeId(branch));
@ -1070,6 +1071,7 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
if (index > 0) {
final List<RouteMatch> matches = matchList.matches.sublist(0, index);
return RouteMatchList(
extra: matchList.extra,
matches: matches,
uri: Uri.parse(matches.last.matchedLocation),
pathParameters: matchList.pathParameters,
@ -1114,15 +1116,10 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
/// the branch (see [StatefulShellBranch.initialLocation]).
void goBranch(int index, {bool initialLocation = false}) {
assert(index >= 0 && index < route.branches.length);
final RouteMatchList? matchlist =
final RouteMatchList? matchList =
initialLocation ? null : _matchListForBranch(index);
if (matchlist != null && matchlist.isNotEmpty) {
final RouteInformation preParsed =
matchlist.toPreParsedRouteInformation();
// TODO(tolo): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use, unnecessary_non_null_assertion
_router.go(preParsed.location!, extra: preParsed.state);
if (matchList != null && matchList.isNotEmpty) {
_router.restore(matchList);
} else {
_router.go(widget._effectiveInitialBranchLocation(index));
}
@ -1169,8 +1166,8 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
/// [RestorableProperty] for enabling state restoration of [RouteMatchList]s.
class _RestorableRouteMatchList extends RestorableProperty<RouteMatchList> {
_RestorableRouteMatchList(RouteMatcher matcher)
: _matchListCodec = RouteMatchListCodec(matcher);
_RestorableRouteMatchList(RouteConfiguration configuration)
: _matchListCodec = RouteMatchListCodec(configuration);
final RouteMatchListCodec _matchListCodec;
@ -1193,13 +1190,15 @@ class _RestorableRouteMatchList extends RestorableProperty<RouteMatchList> {
@override
RouteMatchList fromPrimitives(Object? data) {
return _matchListCodec.decodeMatchList(data) ?? RouteMatchList.empty;
return data == null
? RouteMatchList.empty
: _matchListCodec.decode(data as Map<Object?, Object?>);
}
@override
Object? toPrimitives() {
if (value.isNotEmpty) {
return _matchListCodec.encodeMatchList(value);
return _matchListCodec.encode(value);
}
return null;
}

View File

@ -9,7 +9,7 @@ import 'delegate.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
import 'misc/errors.dart';
import 'misc/inherited_router.dart';
import 'parser.dart';
import 'typedefs.dart';
@ -69,37 +69,39 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
assert(
initialExtra == null || initialLocation != null,
'initialLocation must be set in order to use initialExtra',
) {
),
assert(_debugCheckPath(routes, true)),
assert(
_debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
assert(_debugCheckParentNavigatorKeys(
routes,
navigatorKey == null
? <GlobalKey<NavigatorState>>[]
: <GlobalKey<NavigatorState>>[navigatorKey])) {
setLogging(enabled: debugLogDiagnostics);
WidgetsFlutterBinding.ensureInitialized();
navigatorKey ??= GlobalKey<NavigatorState>();
_routeConfiguration = RouteConfiguration(
configuration = RouteConfiguration(
routes: routes,
topRedirect: redirect ?? (_, __) => null,
redirectLimit: redirectLimit,
navigatorKey: navigatorKey,
);
_routeInformationParser = GoRouteInformationParser(
configuration: _routeConfiguration,
debugRequireGoRouteInformationProvider: true,
routeInformationParser = GoRouteInformationParser(
configuration: configuration,
);
_routeInformationProvider = GoRouteInformationProvider(
initialRouteInformation: RouteInformation(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
location: _effectiveInitialLocation(initialLocation),
state: initialExtra,
),
routeInformationProvider = GoRouteInformationProvider(
initialLocation: _effectiveInitialLocation(initialLocation),
initialExtra: initialExtra,
refreshListenable: refreshListenable,
);
_routerDelegate = GoRouterDelegate(
configuration: _routeConfiguration,
routerDelegate = GoRouterDelegate(
configuration: configuration,
errorPageBuilder: errorPageBuilder,
errorBuilder: errorBuilder,
routerNeglect: routerNeglect,
@ -112,7 +114,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
builderWithNav: (BuildContext context, Widget child) =>
InheritedGoRouter(goRouter: this, child: child),
);
_routerDelegate.addListener(_handleStateMayChange);
routerDelegate.addListener(_handleStateMayChange);
assert(() {
log.info('setting initial location $initialLocation');
@ -120,10 +122,125 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
}());
}
late final RouteConfiguration _routeConfiguration;
late final GoRouteInformationParser _routeInformationParser;
late final GoRouterDelegate _routerDelegate;
late final GoRouteInformationProvider _routeInformationProvider;
static bool _debugCheckPath(List<RouteBase> routes, bool isTopLevel) {
for (final RouteBase route in routes) {
late bool subRouteIsTopLevel;
if (route is GoRoute) {
if (isTopLevel) {
assert(route.path.startsWith('/'),
'top-level path must start with "/": $route');
} else {
assert(!route.path.startsWith('/') && !route.path.endsWith('/'),
'sub-route path may not start or end with /: $route');
}
subRouteIsTopLevel = false;
} else if (route is ShellRouteBase) {
subRouteIsTopLevel = isTopLevel;
}
_debugCheckPath(route.routes, subRouteIsTopLevel);
}
return true;
}
// Check that each parentNavigatorKey refers to either a ShellRoute's
// navigatorKey or the root navigator key.
static bool _debugCheckParentNavigatorKeys(
List<RouteBase> routes, List<GlobalKey<NavigatorState>> allowedKeys) {
for (final RouteBase route in routes) {
if (route is GoRoute) {
final GlobalKey<NavigatorState>? parentKey = route.parentNavigatorKey;
if (parentKey != null) {
// Verify that the root navigator or a ShellRoute ancestor has a
// matching navigator key.
assert(
allowedKeys.contains(parentKey),
'parentNavigatorKey $parentKey must refer to'
" an ancestor ShellRoute's navigatorKey or GoRouter's"
' navigatorKey');
_debugCheckParentNavigatorKeys(
route.routes,
<GlobalKey<NavigatorState>>[
// Once a parentNavigatorKey is used, only that navigator key
// or keys above it can be used.
...allowedKeys.sublist(0, allowedKeys.indexOf(parentKey) + 1),
],
);
} else {
_debugCheckParentNavigatorKeys(
route.routes,
<GlobalKey<NavigatorState>>[
...allowedKeys,
],
);
}
} else if (route is ShellRoute) {
_debugCheckParentNavigatorKeys(
route.routes,
<GlobalKey<NavigatorState>>[...allowedKeys..add(route.navigatorKey)],
);
} else if (route is StatefulShellRoute) {
for (final StatefulShellBranch branch in route.branches) {
assert(
!allowedKeys.contains(branch.navigatorKey),
'StatefulShellBranch must not reuse an ancestor navigatorKey '
'(${branch.navigatorKey})');
_debugCheckParentNavigatorKeys(
branch.routes,
<GlobalKey<NavigatorState>>[
...allowedKeys,
branch.navigatorKey,
],
);
}
}
}
return true;
}
static bool _debugVerifyNoDuplicatePathParameter(
List<RouteBase> routes, Map<String, GoRoute> usedPathParams) {
for (final RouteBase route in routes) {
if (route is! GoRoute) {
continue;
}
for (final String pathParam in route.pathParameters) {
if (usedPathParams.containsKey(pathParam)) {
final bool sameRoute = usedPathParams[pathParam] == route;
throw GoError(
"duplicate path parameter, '$pathParam' found in ${sameRoute ? '$route' : '${usedPathParams[pathParam]}, and $route'}");
}
usedPathParams[pathParam] = route;
}
_debugVerifyNoDuplicatePathParameter(route.routes, usedPathParams);
route.pathParameters.forEach(usedPathParams.remove);
}
return true;
}
/// Whether the imperative API affects browser URL bar.
///
/// The Imperative APIs refer to [push], [pushReplacement], or [Replace].
///
/// If this option is set to true. The URL bar reflects the top-most [GoRoute]
/// regardless the [RouteBase]s underneath.
///
/// If this option is set to false. The URL bar reflects the [RouteBase]s
/// in the current state but ignores any [RouteBase]s that are results of
/// imperative API calls.
///
/// Defaults to false.
///
/// This option is for backward compatibility. It is strongly suggested
/// against setting this value to true, as the URL of the top-most [GoRoute]
/// is not always deeplink-able.
///
/// This option only affects web platform.
static bool optionURLReflectsImperativeAPIs = false;
/// The route configuration used in go_router.
late final RouteConfiguration configuration;
@override
final BackButtonDispatcher backButtonDispatcher;
@ -131,17 +248,15 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// The router delegate. Provide this to the MaterialApp or CupertinoApp's
/// `.router()` constructor
@override
GoRouterDelegate get routerDelegate => _routerDelegate;
late final GoRouterDelegate routerDelegate;
/// The route information provider used by [GoRouter].
@override
GoRouteInformationProvider get routeInformationProvider =>
_routeInformationProvider;
late final GoRouteInformationProvider routeInformationProvider;
/// The route information parser used by [GoRouter].
@override
GoRouteInformationParser get routeInformationParser =>
_routeInformationParser;
late final GoRouteInformationParser routeInformationParser;
/// Gets the current location.
// TODO(chunhtai): deprecates this once go_router_builder is migrated to
@ -150,7 +265,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
String _location = '/';
/// Returns `true` if there is at least two or more route can be pop.
bool canPop() => _routerDelegate.canPop();
bool canPop() => routerDelegate.canPop();
void _handleStateMayChange() {
final String newLocation;
@ -158,12 +273,12 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
routerDelegate.currentConfiguration.matches.last
is ImperativeRouteMatch) {
newLocation = (routerDelegate.currentConfiguration.matches.last
as ImperativeRouteMatch<Object?>)
as ImperativeRouteMatch)
.matches
.uri
.toString();
} else {
newLocation = _routerDelegate.currentConfiguration.uri.toString();
newLocation = routerDelegate.currentConfiguration.uri.toString();
}
if (_location != newLocation) {
_location = newLocation;
@ -178,31 +293,26 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
}) =>
_routeInformationParser.configuration.namedLocation(
configuration.namedLocation(
name,
pathParameters: pathParameters,
queryParameters: queryParameters,
);
/// Get the location for the provided route.
///
/// Builds the absolute path for the route, by concatenating the paths of the
/// route and all its ancestors.
String? locationForRoute(RouteBase route) =>
_routeInformationParser.configuration.locationForRoute(route);
/// Navigate to a URI location w/ optional query parameters, e.g.
/// `/family/f2/person/p1?color=blue`
void go(String location, {Object? extra}) {
assert(() {
log.info('going to $location');
return true;
}());
_routeInformationProvider.value =
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
RouteInformation(location: location, state: extra);
log.info('going to $location');
routeInformationProvider.go(location, extra: extra);
}
/// Restore the RouteMatchList
void restore(RouteMatchList matchList) {
log.info('going to ${matchList.uri}');
routeInformationProvider.restore(
matchList.uri.toString(),
encodedMatchList: RouteMatchListCodec(configuration).encode(matchList),
);
}
/// Navigate to a named route w/ optional parameters, e.g.
@ -230,22 +340,12 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// it as the same page. The page key will be reused. This will preserve the
/// state and not run any page animation.
Future<T?> push<T extends Object?>(String location, {Object? extra}) async {
assert(() {
log.info('pushing $location');
return true;
}());
final RouteMatchList matches =
await _routeInformationParser.parseRouteInformationWithDependencies(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
RouteInformation(location: location, state: extra),
// TODO(chunhtai): avoid accessing the context directly through global key.
// https://github.com/flutter/flutter/issues/99112
_routerDelegate.navigatorKey.currentContext!,
log.info('pushing $location');
return routeInformationProvider.push<T>(
location,
base: routerDelegate.currentConfiguration,
extra: extra,
);
return _routerDelegate.push<T>(matches);
}
/// Push a named route onto the page stack w/ optional parameters, e.g.
@ -271,20 +371,14 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// * [replace] which replaces the top-most page of the page stack but treats
/// it as the same page. The page key will be reused. This will preserve the
/// state and not run any page animation.
void pushReplacement(String location, {Object? extra}) {
routeInformationParser
.parseRouteInformationWithDependencies(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
RouteInformation(location: location, state: extra),
// TODO(chunhtai): avoid accessing the context directly through global key.
// https://github.com/flutter/flutter/issues/99112
_routerDelegate.navigatorKey.currentContext!,
)
.then<void>((RouteMatchList matchList) {
routerDelegate.pushReplacement(matchList);
});
Future<T?> pushReplacement<T extends Object?>(String location,
{Object? extra}) {
log.info('pushReplacement $location');
return routeInformationProvider.pushReplacement<T>(
location,
base: routerDelegate.currentConfiguration,
extra: extra,
);
}
/// Replaces the top-most page of the page stack with the named route w/
@ -294,13 +388,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// See also:
/// * [goNamed] which navigates a named route.
/// * [pushNamed] which pushes a named route onto the page stack.
void pushReplacementNamed(
Future<T?> pushReplacementNamed<T extends Object?>(
String name, {
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) {
pushReplacement(
return pushReplacement<T>(
namedLocation(name,
pathParameters: pathParameters, queryParameters: queryParameters),
extra: extra,
@ -317,20 +411,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// * [push] which pushes the given location onto the page stack.
/// * [pushReplacement] which replaces the top-most page of the page stack but
/// always uses a new page key.
void replace(String location, {Object? extra}) {
routeInformationParser
.parseRouteInformationWithDependencies(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
RouteInformation(location: location, state: extra),
// TODO(chunhtai): avoid accessing the context directly through global key.
// https://github.com/flutter/flutter/issues/99112
_routerDelegate.navigatorKey.currentContext!,
)
.then<void>((RouteMatchList matchList) {
routerDelegate.replace(matchList);
});
Future<T?> replace<T>(String location, {Object? extra}) {
log.info('replace $location');
return routeInformationProvider.replace<T>(
location,
base: routerDelegate.currentConfiguration,
extra: extra,
);
}
/// Replaces the top-most page with the named route and optional parameters,
@ -344,13 +431,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// * [pushNamed] which pushes the given location onto the page stack.
/// * [pushReplacementNamed] which replaces the top-most page of the page
/// stack but always uses a new page key.
void replaceNamed(
Future<T?> replaceNamed<T>(
String name, {
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) {
replace(
return replace(
namedLocation(name,
pathParameters: pathParameters, queryParameters: queryParameters),
extra: extra,
@ -366,7 +453,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
log.info('popping $location');
return true;
}());
_routerDelegate.pop<T>(result);
routerDelegate.pop<T>(result);
}
/// Refresh the route.
@ -375,7 +462,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
log.info('refreshing $location');
return true;
}());
_routeInformationProvider.notifyListeners();
routeInformationProvider.notifyListeners();
}
/// Find the current GoRouter in the widget tree.
@ -395,9 +482,9 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
@override
void dispose() {
_routeInformationProvider.dispose();
_routerDelegate.removeListener(_handleStateMayChange);
_routerDelegate.dispose();
routeInformationProvider.dispose();
routerDelegate.removeListener(_handleStateMayChange);
routerDelegate.dispose();
super.dispose();
}

View File

@ -27,9 +27,6 @@ class GoRouterState {
this.error,
required this.pageKey,
});
// TODO(johnpryan): remove once namedLocation is removed from go_router.
// See https://github.com/flutter/flutter/issues/107729
final RouteConfiguration _configuration;
/// The full location of the route, e.g. /family/f2/person/p1

View File

@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 7.1.1
version: 8.0.0
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

View File

@ -7,7 +7,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/src/builder.dart';
import 'package:go_router/src/configuration.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/matching.dart';
import 'package:go_router/src/router.dart';
import 'test_helpers.dart';
@ -36,8 +35,6 @@ void main() {
RouteMatch(
route: config.routes.first as GoRoute,
matchedLocation: '/',
extra: null,
error: null,
pageKey: const ValueKey<String>('/'),
),
],
@ -121,8 +118,6 @@ void main() {
RouteMatch(
route: config.routes.first as GoRoute,
matchedLocation: '/',
extra: null,
error: null,
pageKey: const ValueKey<String>('/'),
),
],
@ -176,15 +171,11 @@ void main() {
RouteMatch(
route: config.routes.first,
matchedLocation: '',
extra: null,
error: null,
pageKey: const ValueKey<String>(''),
),
RouteMatch(
route: config.routes.first.routes.first,
matchedLocation: '/details',
extra: null,
error: null,
pageKey: const ValueKey<String>('/details'),
),
],
@ -251,8 +242,6 @@ void main() {
RouteMatch(
route: config.routes.first.routes.first as GoRoute,
matchedLocation: '/a/details',
extra: null,
error: null,
pageKey: const ValueKey<String>('/a/details'),
),
],

View File

@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/src/configuration.dart';
import 'package:go_router/src/misc/errors.dart';
import 'test_helpers.dart';
@ -120,7 +121,7 @@ void main() {
},
);
},
throwsAssertionError,
throwsA(isA<GoError>()),
);
});
@ -318,7 +319,7 @@ void main() {
},
);
},
throwsAssertionError,
throwsA(isA<GoError>()),
);
});
@ -373,7 +374,7 @@ void main() {
},
);
},
throwsAssertionError,
throwsA(isA<GoError>()),
);
});
@ -749,7 +750,7 @@ void main() {
return null;
},
),
throwsAssertionError,
throwsA(isA<GoError>()),
);
},
);
@ -857,7 +858,7 @@ void main() {
return null;
},
),
throwsAssertionError,
throwsA(isA<GoError>()),
);
},
);

View File

@ -79,10 +79,13 @@ void main() {
..push('/error');
await tester.pumpAndSettle();
final RouteMatch last = goRouter.routerDelegate.matches.matches.last;
final RouteMatch last =
goRouter.routerDelegate.currentConfiguration.matches.last;
await goRouter.routerDelegate.popRoute();
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(goRouter.routerDelegate.matches.matches.contains(last), false);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
expect(
goRouter.routerDelegate.currentConfiguration.matches.contains(last),
false);
});
testWidgets('pops more than matches count should return false',
@ -100,24 +103,19 @@ void main() {
'It should return different pageKey when push is called',
(WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester);
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
goRouter.push('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches[1].pageKey,
const ValueKey<String>('/a-p0'),
);
goRouter.push('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 3);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
expect(
goRouter.routerDelegate.matches.matches[2].pageKey,
const ValueKey<String>('/a-p1'),
goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
isNot(equals(
goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
);
},
);
@ -134,10 +132,11 @@ void main() {
goRouter.push('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 3);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
expect(
goRouter.routerDelegate.matches.matches[2].pageKey,
const Key('/a-p0'),
goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
isNot(equals(
goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
);
},
);
@ -154,10 +153,11 @@ void main() {
goRouter.push('/c/c2');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 3);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
expect(
goRouter.routerDelegate.matches.matches[2].pageKey,
const Key('/c/c2-p0'),
goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
isNot(equals(
goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
);
},
);
@ -174,10 +174,11 @@ void main() {
goRouter.push('/c');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 3);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
expect(
goRouter.routerDelegate.matches.matches[2].pageKey,
const Key('/c-p1'),
goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
isNot(equals(
goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
);
},
);
@ -190,7 +191,7 @@ void main() {
final GoRouter goRouter = await createGoRouter(tester);
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
expect(goRouter.routerDelegate.canPop(), false);
},
);
@ -201,7 +202,7 @@ void main() {
..push('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(goRouter.routerDelegate.canPop(), true);
},
);
@ -227,22 +228,24 @@ void main() {
goRouter.push('/page-0');
goRouter.routerDelegate.addListener(expectAsync0(() {}));
final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
final RouteMatch last = goRouter.routerDelegate.matches.last;
final RouteMatch first =
goRouter.routerDelegate.currentConfiguration.matches.first;
final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
goRouter.pushReplacement('/page-1');
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.first,
goRouter.routerDelegate.currentConfiguration.matches.first,
first,
reason: 'The first match should still be in the list of matches',
);
expect(
goRouter.routerDelegate.matches.last,
goRouter.routerDelegate.currentConfiguration.last,
isNot(last),
reason: 'The last match should have been removed',
);
expect(
(goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
(goRouter.routerDelegate.currentConfiguration.last
as ImperativeRouteMatch)
.matches
.uri
.toString(),
@ -255,28 +258,26 @@ void main() {
'It should return different pageKey when pushReplacement is called',
(WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester);
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
expect(
goRouter.routerDelegate.matches.matches[0].pageKey,
goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
isNotNull,
);
goRouter.push('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/a-p0'),
);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
final ValueKey<String> prev =
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
goRouter.pushReplacement('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/a-p1'),
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
isNot(equals(prev)),
);
},
);
@ -309,22 +310,24 @@ void main() {
goRouter.pushNamed('page0');
goRouter.routerDelegate.addListener(expectAsync0(() {}));
final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
final RouteMatch last = goRouter.routerDelegate.matches.last;
final RouteMatch first =
goRouter.routerDelegate.currentConfiguration.matches.first;
final RouteMatch last =
goRouter.routerDelegate.currentConfiguration.last;
goRouter.pushReplacementNamed('page1');
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.first,
goRouter.routerDelegate.currentConfiguration.matches.first,
first,
reason: 'The first match should still be in the list of matches',
);
expect(
goRouter.routerDelegate.matches.last,
goRouter.routerDelegate.currentConfiguration.last,
isNot(last),
reason: 'The last match should have been removed',
);
expect(
goRouter.routerDelegate.matches.last,
goRouter.routerDelegate.currentConfiguration.last,
isA<RouteMatch>().having(
(RouteMatch match) => (match.route as GoRoute).name,
'match.route.name',
@ -356,22 +359,24 @@ void main() {
goRouter.push('/page-0');
goRouter.routerDelegate.addListener(expectAsync0(() {}));
final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
final RouteMatch last = goRouter.routerDelegate.matches.last;
final RouteMatch first =
goRouter.routerDelegate.currentConfiguration.matches.first;
final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
goRouter.replace('/page-1');
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.first,
goRouter.routerDelegate.currentConfiguration.matches.first,
first,
reason: 'The first match should still be in the list of matches',
);
expect(
goRouter.routerDelegate.matches.last,
goRouter.routerDelegate.currentConfiguration.last,
isNot(last),
reason: 'The last match should have been removed',
);
expect(
(goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
(goRouter.routerDelegate.currentConfiguration.last
as ImperativeRouteMatch)
.matches
.uri
.toString(),
@ -384,28 +389,26 @@ void main() {
'It should use the same pageKey when replace is called (with the same path)',
(WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester);
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
expect(
goRouter.routerDelegate.matches.matches[0].pageKey,
goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
isNotNull,
);
goRouter.push('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/a-p0'),
);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
final ValueKey<String> prev =
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
goRouter.replace('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/a-p0'),
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
prev,
);
},
);
@ -414,28 +417,26 @@ void main() {
'It should use the same pageKey when replace is called (with a different path)',
(WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester);
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
expect(
goRouter.routerDelegate.matches.matches[0].pageKey,
goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
isNotNull,
);
goRouter.push('/a');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/a-p0'),
);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
final ValueKey<String> prev =
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
goRouter.replace('/');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/a-p0'),
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
prev,
);
},
);
@ -479,22 +480,24 @@ void main() {
goRouter.pushNamed('page0');
goRouter.routerDelegate.addListener(expectAsync0(() {}));
final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
final RouteMatch last = goRouter.routerDelegate.matches.last;
final RouteMatch first =
goRouter.routerDelegate.currentConfiguration.matches.first;
final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
goRouter.replaceNamed('page1');
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.first,
goRouter.routerDelegate.currentConfiguration.matches.first,
first,
reason: 'The first match should still be in the list of matches',
);
expect(
goRouter.routerDelegate.matches.last,
goRouter.routerDelegate.currentConfiguration.last,
isNot(last),
reason: 'The last match should have been removed',
);
expect(
(goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
(goRouter.routerDelegate.currentConfiguration.last
as ImperativeRouteMatch)
.matches
.uri
.toString(),
@ -507,28 +510,26 @@ void main() {
'It should use the same pageKey when replace is called with the same path',
(WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester);
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
expect(
goRouter.routerDelegate.matches.matches.first.pageKey,
goRouter.routerDelegate.currentConfiguration.matches.first.pageKey,
isNotNull,
);
goRouter.pushNamed('page0');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/page-0-p0'),
);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
final ValueKey<String> prev =
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
goRouter.replaceNamed('page0');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/page-0-p0'),
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
prev,
);
},
);
@ -537,28 +538,26 @@ void main() {
'It should use a new pageKey when replace is called with a different path',
(WidgetTester tester) async {
final GoRouter goRouter = await createGoRouter(tester);
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
expect(
goRouter.routerDelegate.matches.matches.first.pageKey,
goRouter.routerDelegate.currentConfiguration.matches.first.pageKey,
isNotNull,
);
goRouter.pushNamed('page0');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/page-0-p0'),
);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
final ValueKey<String> prev =
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
goRouter.replaceNamed('home');
await tester.pumpAndSettle();
expect(goRouter.routerDelegate.matches.matches.length, 2);
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
expect(
goRouter.routerDelegate.matches.matches.last.pageKey,
const ValueKey<String>('/page-0-p0'),
goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
prev,
);
},
);

View File

@ -6,13 +6,13 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/matching.dart';
import 'package:logging/logging.dart';
import 'test_helpers.dart';
@ -46,7 +46,7 @@ void main() {
];
final GoRouter router = await createRouter(routes, tester);
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(matches.matches, hasLength(1));
expect(matches.uri.toString(), '/');
expect(find.byType(HomeScreen), findsOneWidget);
@ -61,7 +61,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/');
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect((matches.first.route as GoRoute).name, '1');
expect(find.byType(DummyScreen), findsOneWidget);
@ -129,8 +130,9 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/foo');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
expect(find.byType(TestErrorScreen), findsOneWidget);
});
@ -149,7 +151,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/login');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
@ -178,7 +181,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/login');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
@ -202,7 +206,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/login/');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
@ -221,7 +226,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/profile/');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/profile/foo');
expect(find.byType(DummyScreen), findsOneWidget);
@ -240,7 +246,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/profile/?bar=baz');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(matches.first.matchedLocation, '/profile/foo');
expect(find.byType(DummyScreen), findsOneWidget);
@ -352,7 +359,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/login');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches.length, 2);
expect(matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
@ -390,7 +398,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
{
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches =
router.routerDelegate.currentConfiguration;
expect(matches.matches, hasLength(1));
expect(matches.uri.toString(), '/');
expect(find.byType(HomeScreen), findsOneWidget);
@ -399,7 +408,8 @@ void main() {
router.go('/login');
await tester.pumpAndSettle();
{
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches =
router.routerDelegate.currentConfiguration;
expect(matches.matches.length, 2);
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
@ -410,7 +420,8 @@ void main() {
router.go('/family/f2');
await tester.pumpAndSettle();
{
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches =
router.routerDelegate.currentConfiguration;
expect(matches.matches.length, 2);
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
@ -421,7 +432,8 @@ void main() {
router.go('/family/f2/person/p1');
await tester.pumpAndSettle();
{
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches =
router.routerDelegate.currentConfiguration;
expect(matches.matches.length, 3);
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
@ -469,19 +481,20 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/bar');
await tester.pumpAndSettle();
List<RouteMatch> matches = router.routerDelegate.matches.matches;
List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(2));
expect(find.byType(Page1Screen), findsOneWidget);
router.go('/foo/bar');
await tester.pumpAndSettle();
matches = router.routerDelegate.matches.matches;
matches = router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(2));
expect(find.byType(FamilyScreen), findsOneWidget);
router.go('/foo');
await tester.pumpAndSettle();
matches = router.routerDelegate.matches.matches;
matches = router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(2));
expect(find.byType(Page2Screen), findsOneWidget);
});
@ -592,7 +605,8 @@ void main() {
const String loc = '/FaMiLy/f2';
router.go(loc);
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
// NOTE: match the lower case, since location is canonicalized to match the
// path case whereas the location can be any case; so long as the path
@ -616,7 +630,8 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.go('/user');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(find.byType(DummyScreen), findsOneWidget);
});
@ -973,6 +988,7 @@ void main() {
group('report correct url', () {
final List<MethodCall> log = <MethodCall>[];
setUp(() {
GoRouter.optionURLReflectsImperativeAPIs = false;
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.navigation,
@ -982,12 +998,43 @@ void main() {
});
});
tearDown(() {
GoRouter.optionURLReflectsImperativeAPIs = false;
_ambiguate(TestDefaultBinaryMessengerBinding.instance)!
.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.navigation, null);
log.clear();
});
testWidgets('on push with optionURLReflectImperativeAPIs = true',
(WidgetTester tester) async {
GoRouter.optionURLReflectsImperativeAPIs = true;
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const DummyScreen(),
),
GoRoute(
path: '/settings',
builder: (_, __) => const DummyScreen(),
),
];
final GoRouter router = await createRouter(routes, tester);
log.clear();
router.push('/settings');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
await tester.pumpAndSettle();
final ImperativeRouteMatch match = router
.routerDelegate.currentConfiguration.last as ImperativeRouteMatch;
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
IsRouteUpdateCall('/settings', false, codec.encode(match.matches)),
]);
GoRouter.optionURLReflectsImperativeAPIs = false;
});
testWidgets('on push', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
@ -1004,10 +1051,13 @@ void main() {
log.clear();
router.push('/settings');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
const IsRouteUpdateCall('/settings', false, null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
@ -1026,13 +1076,15 @@ void main() {
final GoRouter router =
await createRouter(routes, tester, initialLocation: '/settings');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
const IsRouteUpdateCall('/', false, null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
@ -1056,14 +1108,16 @@ void main() {
final GoRouter router = await createRouter(routes, tester,
initialLocation: '/settings/profile');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
log.clear();
router.pop();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
const IsRouteUpdateCall('/', false, null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
@ -1082,13 +1136,15 @@ void main() {
final GoRouter router =
await createRouter(routes, tester, initialLocation: '/settings/123');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
const IsRouteUpdateCall('/', false, null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
@ -1108,13 +1164,15 @@ void main() {
final GoRouter router =
await createRouter(routes, tester, initialLocation: '/123/');
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
log.clear();
router.pop();
await tester.pumpAndSettle();
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
const IsRouteUpdateCall('/', false, null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
@ -1165,12 +1223,15 @@ void main() {
),
];
await createRouter(routes, tester,
final GoRouter router = await createRouter(routes, tester,
initialLocation: '/b/c', navigatorKey: rootNavigatorKey);
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
expect(find.text('Screen C'), findsOneWidget);
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
const IsRouteUpdateCall('/b/c', false, null),
IsRouteUpdateCall('/b/c', true,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
log.clear();
@ -1180,10 +1241,70 @@ void main() {
expect(find.text('Home'), findsOneWidget);
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
const IsRouteUpdateCall('/', false, null),
IsRouteUpdateCall('/', false,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
testWidgets('can handle route information update from browser',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (_, __) => const DummyScreen(key: ValueKey<String>('home')),
routes: <RouteBase>[
GoRoute(
path: 'settings',
builder: (_, GoRouterState state) =>
DummyScreen(key: ValueKey<String>('settings-${state.extra}')),
),
],
),
];
final GoRouter router = await createRouter(routes, tester);
expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
router.push('/settings', extra: 0);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('settings-0')), findsOneWidget);
log.clear();
router.push('/settings', extra: 1);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('settings-1')), findsOneWidget);
final Map<Object?, Object?> arguments =
log.last.arguments as Map<Object?, Object?>;
// Stores the state after the last push. This should contain the encoded
// RouteMatchList.
final Object? state =
(log.last.arguments as Map<Object?, Object?>)['state'];
final String location =
(arguments['location'] ?? arguments['uri']!) as String;
router.go('/');
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
router.routeInformationProvider.didPushRouteInformation(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
RouteInformation(location: location, state: state));
await tester.pumpAndSettle();
// Make sure it has all the imperative routes.
expect(find.byKey(const ValueKey<String>('settings-1')), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('settings-0')), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
});
testWidgets('works correctly with async redirect',
(WidgetTester tester) async {
final UniqueKey login = UniqueKey();
@ -1198,10 +1319,13 @@ void main() {
),
];
final Completer<void> completer = Completer<void>();
await createRouter(routes, tester, redirect: (_, __) async {
final GoRouter router =
await createRouter(routes, tester, redirect: (_, __) async {
await completer.future;
return '/login';
});
final RouteMatchListCodec codec =
RouteMatchListCodec(router.configuration);
await tester.pumpAndSettle();
expect(find.byKey(login), findsNothing);
expect(tester.takeException(), isNull);
@ -1214,7 +1338,8 @@ void main() {
expect(tester.takeException(), isNull);
expect(log, <Object>[
isMethodCall('selectMultiEntryHistory', arguments: null),
const IsRouteUpdateCall('/login', false, null),
IsRouteUpdateCall('/login', true,
codec.encode(router.routerDelegate.currentConfiguration)),
]);
});
});
@ -1487,7 +1612,7 @@ void main() {
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(find.byType(DummyScreen), findsOneWidget);
expect(matches.pathParameters['param1'], param1);
});
@ -1511,7 +1636,7 @@ void main() {
queryParameters: <String, String>{'param1': param1});
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(find.byType(DummyScreen), findsOneWidget);
expect(matches.uri.queryParameters['param1'], param1);
});
@ -1766,8 +1891,9 @@ void main() {
? '/'
: null);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
expect(find.byType(TestErrorScreen), findsOneWidget);
final TestErrorScreen screen =
tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
@ -1791,8 +1917,9 @@ void main() {
tester,
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
expect(find.byType(TestErrorScreen), findsOneWidget);
final TestErrorScreen screen =
tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
@ -1813,8 +1940,9 @@ void main() {
state.matchedLocation == '/' ? '/login' : null,
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
expect(find.byType(TestErrorScreen), findsOneWidget);
final TestErrorScreen screen =
tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
@ -1834,8 +1962,9 @@ void main() {
: null,
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
expect(find.byType(TestErrorScreen), findsOneWidget);
final TestErrorScreen screen =
tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
@ -1895,7 +2024,8 @@ void main() {
},
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(find.byType(LoginScreen), findsOneWidget);
});
@ -1924,7 +2054,8 @@ void main() {
initialLocation: loc,
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expect(find.byType(HomeScreen), findsOneWidget);
});
@ -1965,7 +2096,8 @@ void main() {
initialLocation: '/family/f2/person/p1',
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches.length, 3);
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(find.byType(FamilyScreen, skipOffstage: false), findsOneWidget);
@ -1984,14 +2116,56 @@ void main() {
redirectLimit: 10,
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
expect(find.byType(TestErrorScreen), findsOneWidget);
final TestErrorScreen screen =
tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
expect(screen.ex, isNotNull);
});
testWidgets('can push error page', (WidgetTester tester) async {
final GoRouter router = await createRouter(
<GoRoute>[
GoRoute(path: '/', builder: (_, __) => const Text('/')),
],
tester,
errorBuilder: (_, GoRouterState state) {
return Text(state.location);
},
);
expect(find.text('/'), findsOneWidget);
router.push('/error1');
await tester.pumpAndSettle();
expect(find.text('/'), findsNothing);
expect(find.text('/error1'), findsOneWidget);
router.push('/error2');
await tester.pumpAndSettle();
expect(find.text('/'), findsNothing);
expect(find.text('/error1'), findsNothing);
expect(find.text('/error2'), findsOneWidget);
router.pop();
await tester.pumpAndSettle();
expect(find.text('/'), findsNothing);
expect(find.text('/error1'), findsOneWidget);
expect(find.text('/error2'), findsNothing);
router.pop();
await tester.pumpAndSettle();
expect(find.text('/'), findsOneWidget);
expect(find.text('/error1'), findsNothing);
expect(find.text('/error2'), findsNothing);
});
testWidgets('extra not null in redirect', (WidgetTester tester) async {
bool isCallTopRedirect = false;
bool isCallRouteRedirect = false;
@ -2231,7 +2405,8 @@ void main() {
final String loc = '/family/$fid';
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches =
router.routerDelegate.currentConfiguration;
expect(router.location, loc);
expect(matches.matches, hasLength(1));
@ -2260,7 +2435,8 @@ void main() {
final String loc = '/family?fid=$fid';
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches =
router.routerDelegate.currentConfiguration;
expect(router.location, loc);
expect(matches.matches, hasLength(1));
@ -2287,7 +2463,7 @@ void main() {
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(find.byType(DummyScreen), findsOneWidget);
expect(matches.pathParameters['param1'], param1);
});
@ -2309,7 +2485,7 @@ void main() {
router.go('/page1?param1=$param1');
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(find.byType(DummyScreen), findsOneWidget);
expect(matches.uri.queryParameters['param1'], param1);
@ -2317,7 +2493,8 @@ void main() {
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches2 = router.routerDelegate.matches;
final RouteMatchList matches2 =
router.routerDelegate.currentConfiguration;
expect(find.byType(DummyScreen), findsOneWidget);
expect(matches2.uri.queryParameters['param1'], param1);
});
@ -2358,7 +2535,7 @@ void main() {
tester,
initialLocation: '/?id=0&id=1',
);
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(matches.matches, hasLength(1));
expect(matches.fullPath, '/');
expect(find.byType(HomeScreen), findsOneWidget);
@ -2381,7 +2558,7 @@ void main() {
router.go('/0?id=1');
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(matches.matches, hasLength(1));
expect(matches.fullPath, '/:id');
expect(find.byType(HomeScreen), findsOneWidget);
@ -2493,13 +2670,13 @@ void main() {
router.push(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(router.location, loc);
expect(matches.matches, hasLength(2));
expect(find.byType(PersonScreen), findsOneWidget);
final ImperativeRouteMatch<Object?> imperativeRouteMatch =
matches.matches.last as ImperativeRouteMatch<Object?>;
final ImperativeRouteMatch imperativeRouteMatch =
matches.matches.last as ImperativeRouteMatch;
expect(imperativeRouteMatch.matches.pathParameters['fid'], fid);
expect(imperativeRouteMatch.matches.pathParameters['pid'], pid);
});
@ -2563,7 +2740,7 @@ void main() {
router.go(loc);
await tester.pumpAndSettle();
RouteMatchList matches = router.routerDelegate.matches;
RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(router.location, loc);
expect(matches.matches, hasLength(4));
@ -2580,7 +2757,7 @@ void main() {
await tester.pumpAndSettle();
expect(find.text('Screen A'), findsNothing);
expect(find.byType(PersonScreen), findsOneWidget);
matches = router.routerDelegate.matches;
matches = router.routerDelegate.currentConfiguration;
expect(matches.pathParameters['fid'], fid);
expect(matches.pathParameters['pid'], pid);
});
@ -2623,7 +2800,8 @@ void main() {
'q2': <String>['v2', 'v3'],
});
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expectLocationWithQueryParams(router.location);
@ -2673,7 +2851,8 @@ void main() {
router.go('/page?q1=v1&q2=v2&q2=v3');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expectLocationWithQueryParams(router.location);
@ -2722,7 +2901,8 @@ void main() {
router.go('/page?q1=v1&q2=v2&q2=v3');
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
final List<RouteMatch> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(1));
expectLocationWithQueryParams(router.location);
@ -4681,7 +4861,11 @@ class IsRouteUpdateCall extends Matcher {
if (arguments['uri'] != uri && arguments['location'] != uri) {
return false;
}
return arguments['state'] == state && arguments['replace'] == replace;
if (!const DeepCollectionEquality().equals(arguments['state'], state)) {
return false;
}
return arguments['replace'] == replace;
}
@override

View File

@ -6,31 +6,31 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/src/information_provider.dart';
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
const RouteInformation initialRoute = RouteInformation(location: '/');
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
const RouteInformation newRoute = RouteInformation(location: '/new');
const String initialRoute = '/';
const String newRoute = '/new';
void main() {
group('GoRouteInformationProvider', () {
testWidgets('notifies its listeners when set by the app',
(WidgetTester tester) async {
late final GoRouteInformationProvider provider =
GoRouteInformationProvider(initialRouteInformation: initialRoute);
GoRouteInformationProvider(
initialLocation: initialRoute, initialExtra: null);
provider.addListener(expectAsync0(() {}));
provider.value = newRoute;
provider.go(newRoute);
});
testWidgets('notifies its listeners when set by the platform',
(WidgetTester tester) async {
late final GoRouteInformationProvider provider =
GoRouteInformationProvider(initialRouteInformation: initialRoute);
GoRouteInformationProvider(
initialLocation: initialRoute, initialExtra: null);
provider.addListener(expectAsync0(() {}));
provider.didPushRouteInformation(newRoute);
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore_for_file: deprecated_member_use
provider
.didPushRouteInformation(const RouteInformation(location: newRoute));
});
});
}

View File

@ -20,7 +20,6 @@ void main() {
remainingLocation: '/users/123',
matchedLocation: '',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
if (match == null) {
fail('Null match');
@ -28,8 +27,6 @@ void main() {
expect(match.route, route);
expect(match.matchedLocation, '/users/123');
expect(pathParameters['userId'], '123');
expect(match.extra, const _Extra('foo'));
expect(match.error, isNull);
expect(match.pageKey, isNotNull);
});
@ -44,7 +41,6 @@ void main() {
remainingLocation: 'users/123',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
if (match == null) {
fail('Null match');
@ -52,8 +48,6 @@ void main() {
expect(match.route, route);
expect(match.matchedLocation, '/home/users/123');
expect(pathParameters['userId'], '123');
expect(match.extra, const _Extra('foo'));
expect(match.error, isNull);
expect(match.pageKey, isNotNull);
});
@ -73,7 +67,6 @@ void main() {
remainingLocation: 'users/123',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
if (match == null) {
fail('Null match');
@ -97,7 +90,6 @@ void main() {
remainingLocation: 'users/123',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
final RouteMatch? match2 = RouteMatch.match(
@ -105,7 +97,6 @@ void main() {
remainingLocation: 'users/1234',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo1'),
);
expect(match1!.pageKey, match2!.pageKey);
@ -122,7 +113,6 @@ void main() {
remainingLocation: 'users/123',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
final RouteMatch? match2 = RouteMatch.match(
@ -130,7 +120,6 @@ void main() {
remainingLocation: 'users/1234',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo1'),
);
expect(match1!.pageKey, match2!.pageKey);
@ -138,21 +127,6 @@ void main() {
});
}
@immutable
class _Extra {
const _Extra(this.value);
final String value;
@override
bool operator ==(Object other) {
return other is _Extra && other.value == value;
}
@override
int get hashCode => value.hashCode;
}
Widget _builder(BuildContext context, GoRouterState state) =>
const Placeholder();

View File

@ -2,11 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/src/configuration.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/matching.dart';
import 'package:go_router/src/router.dart';
import 'test_helpers.dart';
@ -25,7 +26,7 @@ void main() {
router.go('/page-0');
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(matches.toString(), contains('/page-0'));
});
@ -41,7 +42,6 @@ void main() {
remainingLocation: '/page-0',
matchedLocation: '',
pathParameters: params1,
extra: null,
)!;
final Map<String, String> params2 = <String, String>{};
@ -50,7 +50,6 @@ void main() {
remainingLocation: '/page-0',
matchedLocation: '',
pathParameters: params2,
extra: null,
)!;
final RouteMatchList matches1 = RouteMatchList(
@ -93,16 +92,17 @@ void main() {
navigatorKey: GlobalKey<NavigatorState>(),
topRedirect: (_, __) => null,
);
final RouteMatcher matcher = RouteMatcher(configuration);
final RouteMatchListCodec codec = RouteMatchListCodec(matcher);
final RouteMatchListCodec codec = RouteMatchListCodec(configuration);
final RouteMatchList list1 = matcher.findMatch('/a');
final RouteMatchList list2 = matcher.findMatch('/b');
list1.push(ImperativeRouteMatch<Object?>(
pageKey: const ValueKey<String>('/b-p0'), matches: list2));
final RouteMatchList list1 = configuration.findMatch('/a');
final RouteMatchList list2 = configuration.findMatch('/b');
list1.push(ImperativeRouteMatch(
pageKey: const ValueKey<String>('/b-p0'),
matches: list2,
completer: Completer<Object?>()));
final Object? encoded = codec.encodeMatchList(list1);
final RouteMatchList? decoded = codec.decodeMatchList(encoded);
final Map<Object?, Object?> encoded = codec.encode(list1);
final RouteMatchList decoded = codec.decode(encoded);
expect(decoded, isNotNull);
expect(decoded, equals(list1));

View File

@ -6,15 +6,18 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/configuration.dart';
import 'package:go_router/src/information_provider.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/matching.dart';
import 'package:go_router/src/parser.dart';
RouteInformation createRouteInformation(String location, [Object? state]) {
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
return RouteInformation(location: location, state: state);
RouteInformation createRouteInformation(String location, [Object? extra]) {
return RouteInformation(
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use
location: location,
state:
RouteInformationState<void>(type: NavigatingType.go, extra: extra));
}
void main() {
@ -64,7 +67,7 @@ void main() {
List<RouteMatch> matches = matchesObj.matches;
expect(matches.length, 1);
expect(matchesObj.uri.toString(), '/');
expect(matches[0].extra, isNull);
expect(matchesObj.extra, isNull);
expect(matches[0].matchedLocation, '/');
expect(matches[0].route, routes[0]);
@ -74,11 +77,10 @@ void main() {
matches = matchesObj.matches;
expect(matches.length, 2);
expect(matchesObj.uri.toString(), '/abc?def=ghi');
expect(matches[0].extra, extra);
expect(matchesObj.extra, extra);
expect(matches[0].matchedLocation, '/');
expect(matches[0].route, routes[0]);
expect(matches[1].extra, extra);
expect(matches[1].matchedLocation, '/abc');
expect(matches[1].route, routes[0].routes[0]);
});
@ -195,11 +197,10 @@ void main() {
await parser.parseRouteInformationWithDependencies(
createRouteInformation('/def'), context);
final List<RouteMatch> matches = matchesObj.matches;
expect(matches.length, 1);
expect(matches.length, 0);
expect(matchesObj.uri.toString(), '/def');
expect(matches[0].extra, isNull);
expect(matches[0].matchedLocation, '/def');
expect(matches[0].error!.toString(),
expect(matchesObj.extra, isNull);
expect(matchesObj.error!.toString(),
'Exception: no routes for location: /def');
});
@ -267,10 +268,9 @@ void main() {
expect(matchesObj.pathParameters.length, 2);
expect(matchesObj.pathParameters['uid'], '123');
expect(matchesObj.pathParameters['fid'], '456');
expect(matches[0].extra, isNull);
expect(matchesObj.extra, isNull);
expect(matches[0].matchedLocation, '/');
expect(matches[1].extra, isNull);
expect(matches[1].matchedLocation, '/123/family/456');
});
@ -399,8 +399,8 @@ void main() {
createRouteInformation('/abd'), context);
final List<RouteMatch> matches = matchesObj.matches;
expect(matches, hasLength(1));
expect(matches.first.error, isNotNull);
expect(matches, hasLength(0));
expect(matchesObj.error, isNotNull);
});
testWidgets('Creates a match for ShellRoute', (WidgetTester tester) async {
@ -444,6 +444,6 @@ void main() {
final List<RouteMatch> matches = matchesObj.matches;
expect(matches, hasLength(2));
expect(matches.first.error, isNull);
expect(matchesObj.error, isNull);
});
}

View File

@ -301,8 +301,6 @@ RouteMatch createRouteMatch(RouteBase route, String location) {
return RouteMatch(
route: route,
matchedLocation: location,
extra: null,
error: null,
pageKey: ValueKey<String>(location),
);
}