diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 4a0d29b6c6..0bba2a21f2 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -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 diff --git a/packages/go_router/README.md b/packages/go_router/README.md index eae5297abd..9beccbd72a 100644 --- a/packages/go_router/README.md +++ b/packages/go_router/README.md @@ -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) diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index f151251707..d68958cbaa 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -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, GoRouterState> newRegistry = - , 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, GoRouterState> newRegistry = + , 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 navigatorKey, Map, GoRouterState> registry) { - final Map, List>> keyToPage = - , List>>{}; - try { + final Map, List>> keyToPage; + if (matchList.isError) { + keyToPage = , List>>{ + navigatorKey: >[ + _buildErrorPage( + context, _buildErrorState(matchList.error!, matchList.uri)), + ] + }; + } else { + keyToPage = , List>>{}; _buildRecursive(context, matchList, 0, pagePopContext, routerNeglect, keyToPage, navigatorKey, registry); // Every Page should have a corresponding RouteMatch. assert(keyToPage.values.flattened.every((Page page) => pagePopContext.getRouteMatchForPage(page) != null)); - return keyToPage[navigatorKey]!; - } on _RouteBuilderError catch (e) { - return >[ - _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 activeKeys = keyToPage.keys.toSet() - ..addAll(_nestedStatefulNavigatorKeys(matchList)); - _goHeroCache.removeWhere( - (GlobalKey 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 activeKeys = keyToPage.keys.toSet() + ..addAll(_nestedStatefulNavigatorKeys(matchList)); + _goHeroCache.removeWhere( + (GlobalKey key, _) => !activeKeys.contains(key)); + return keyToPage[navigatorKey]!; } static Set> _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? page; - if (route is GoRoute) { + if (state.error != null) { + page = _buildErrorPage(context, state); + keyToPages.putIfAbsent(navigatorKey, () => >[]).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.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 navigatorKey) { - return _buildNavigator( - (Route route, dynamic result) => onPopPage(route, result, null), - >[ - _buildErrorPage(context, e, uri), - ], - navigatorKey, - ); - } - - /// Builds a an error page. - Page _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('error'), + error: error, + pageKey: ValueKey('$location(error)'), ); + } + + /// Builds a an error page. + Page _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 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); diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 07d02d469c..f2e9d84617 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -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, {})), assert(_debugCheckParentNavigatorKeys( routes, >[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 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 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 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 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 [], + error: error, + uri: uri, + pathParameters: const {}, + ); + } + /// The list of top level routes used by [GoRouterDelegate]. final List routes; /// The limit for the number of consecutive redirects. final int redirectLimit; + /// The global key for top level navigator. + final GlobalKey navigatorKey; + /// Top level page redirect. final GoRouterRedirect topRedirect; - /// The key to use when building the root [Navigator]. - final GlobalKey navigatorKey; - final Map _nameToPath = {}; /// 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 pathParameters = {}; + final List? 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? _getLocRouteMatches( + Uri uri, Map pathParameters) { + final List? result = _getLocRouteRecursively( + location: uri.path, + remainingLocation: uri.path, + matchedLocation: '', + pathParameters: pathParameters, + routes: routes, + ); + return result; + } + + List? _getLocRouteRecursively({ + required String location, + required String remainingLocation, + required String matchedLocation, + required Map pathParameters, + required List routes, + }) { + List? result; + late Map subPathParameters; + // find the set of matches at this level of the tree + for (final RouteBase route in routes) { + subPathParameters = {}; + + 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 = [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? 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 = [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 redirect( + BuildContext context, FutureOr prevMatchListFuture, + {required List redirectHistory}) { + FutureOr processRedirect(RouteMatchList prevMatchList) { + final String prevLocation = prevMatchList.uri.toString(); + FutureOr 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 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 routeLevelRedirectResult = + _getRouteLevelRedirect(context, prevMatchList, 0); + if (routeLevelRedirectResult is String?) { + return processRouteLevelRedirect(routeLevelRedirectResult); + } + return routeLevelRedirectResult + .then(processRouteLevelRedirect); + } + + redirectHistory.add(prevMatchList); + // Check for top-level redirect + final FutureOr 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('topLevel'), + ), + ); + + if (topRedirectResult is String?) { + return processTopLevelRedirect(topRedirectResult); + } + return topRedirectResult.then(processTopLevelRedirect); + } + + if (prevMatchListFuture is RouteMatchList) { + return processRedirect(prevMatchListFuture); + } + return prevMatchListFuture.then(processRedirect); + } + + FutureOr _getRouteLevelRedirect( + BuildContext context, + RouteMatchList matchList, + int currentCheckIndex, + ) { + if (currentCheckIndex >= matchList.matches.length) { + return null; + } + final RouteMatch match = matchList.matches[currentCheckIndex]; + FutureOr processRouteRedirect(String? newLocation) => + newLocation ?? + _getRouteLevelRedirect(context, matchList, currentCheckIndex + 1); + final RouteBase route = match.route; + FutureOr 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(processRouteRedirect); + } + + RouteMatchList _getNewMatches( + String newLocation, + Uri previousLocation, + List 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 redirects, + RouteMatchList newMatch, + Uri prevLocation, + ) { + if (redirects.contains(newMatch)) { + throw RedirectionError('redirect loop detected', + [...redirects, newMatch], prevLocation); + } + if (redirects.length > redirectLimit) { + throw RedirectionError('too many redirects', + [...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 diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart index 985dce389d..2ba3729ef9 100644 --- a/packages/go_router/lib/src/delegate.dart +++ b/packages/go_router/lib/src/delegate.dart @@ -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 /// 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 _pushCounts = {}; - _NavigatorStateIterator _createNavigatorStateIterator() => - _NavigatorStateIterator(_matchList, navigatorKey.currentState!); + _NavigatorStateIterator(currentConfiguration, navigatorKey.currentState!); @override Future popRoute() async { @@ -78,45 +62,6 @@ class GoRouterDelegate extends RouterDelegate return false; } - ValueKey _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('$path-p$count'); - } - - Future _push( - RouteMatchList matches, ValueKey pageKey) async { - final ImperativeRouteMatch newPageKeyMatch = ImperativeRouteMatch( - 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 push(RouteMatchList matches) async { - assert(matches.last.route is! ShellRoute); - - final ValueKey pageKey = _getNewKeyForPath(matches.fullPath); - final Future 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 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 if (match is ImperativeRouteMatch) { match.complete(result); } - _remove(match!); + currentConfiguration = currentConfiguration.remove(match!); notifyListeners(); assert(() { _debugAssertMatchListNotEmpty(); @@ -166,57 +111,19 @@ class GoRouterDelegate extends RouterDelegate 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 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 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 /// For use by the Router architecture as part of the RouterDelegate. @override Future 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 diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index d78e5bc83d..0f61e742bc 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -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 { + /// 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? 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( + 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 push(String location, + {required RouteMatchList base, Object? extra}) { + final Completer completer = Completer(); + _setValue( + location, + RouteInformationState( + 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( + 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 pushReplacement(String location, + {required RouteMatchList base, Object? extra}) { + final Completer completer = Completer(); + _setValue( + location, + RouteInformationState( + extra: extra, + baseRouteMatchList: base, + completer: completer, + type: NavigatingType.pushReplacement, + ), + ); + return completer.future; + } + + /// Replaces the top-most route match from `base` with the `location`. + Future replace(String location, + {required RouteMatchList base, Object? extra}) { + final Completer completer = Completer(); + _setValue( + location, + RouteInformationState( + 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(type: NavigatingType.go), + ); + _valueInEngine = _kEmptyRouteInformation; + } notifyListeners(); } @@ -113,9 +263,6 @@ class GoRouteInformationProvider extends RouteInformationProvider @override Future 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(true); } diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index 24aa0587bc..e04833132e 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -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 pathParameters, - required Object? extra, }) { if (route is ShellRouteBase) { return RouteMatch( route: route, matchedLocation: remainingLocation, - extra: extra, - error: null, pageKey: ValueKey(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(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 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 extends RouteMatch { +class ImperativeRouteMatch extends RouteMatch { /// Constructor for [ImperativeRouteMatch]. - ImperativeRouteMatch({ - required super.pageKey, - required this.matches, - }) : _completer = Completer(), - 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 _completer; + final Completer 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 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 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 [], + uri: Uri(), + pathParameters: const {}); + + /// The route matches. + final List matches; + + /// Parameters for the matched route, URI-encoded. + /// + /// The parameters only reflects [RouteMatch]s that are not + /// [ImperativeRouteMatch]. + final Map 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 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: [...matches, match]); + } + + /// Returns a new instance of RouteMatchList with the input `match` removed + /// from the current instance. + RouteMatchList remove(RouteMatch match) { + final List 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 newParameters = []; + patternToRegExp(fullPath, newParameters); + final Set validParameters = newParameters.toSet(); + final Map newPathParameters = + Map.fromEntries( + pathParameters.entries.where((MapEntry 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 get routes => matches.map((RouteMatch e) => e.route).toList(); + + RouteMatchList _copyWith({ + List? matches, + Uri? uri, + Map? 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().equals(matches, other.matches) && + const MapEquality() + .equals(pathParameters, other.pathParameters); + } + + @override + int get hashCode { + return Object.hash( + Object.hashAll(matches), + uri, + extra, + error, + Object.hashAllUnordered( + pathParameters.entries.map((MapEntry 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> { + /// 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> encoder = + const _RouteMatchListEncoder(); + + @override + final Converter, RouteMatchList> decoder; +} + +class _RouteMatchListEncoder + extends Converter> { + const _RouteMatchListEncoder(); + @override + Map convert(RouteMatchList input) { + final List> imperativeMatches = input.matches + .whereType() + .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 _toPrimitives(String location, Object? extra, + {List>? imperativeMatches, String? pageKey}) { + String? encodedExtra; + try { + encodedExtra = json.encoder.convert(extra); + } on JsonUnsupportedObjectError {/* give up if not serializable */} + return { + 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, RouteMatchList> { + _RouteMatchListDecoder(this.configuration); + + final RouteConfiguration configuration; + + @override + RouteMatchList convert(Map 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? imperativeMatches = + input[RouteMatchListCodec._imperativeMatchesKey] as List?; + if (imperativeMatches != null) { + for (final Map encodedImperativeMatch + in imperativeMatches.whereType>()) { + final RouteMatchList imperativeMatchList = + convert(encodedImperativeMatch); + final ValueKey pageKey = ValueKey( + 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(), + matches: imperativeMatchList, + ); + matchList = matchList.push(imperativeMatch); + } + } + + return matchList; + } +} diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart deleted file mode 100644 index b809ccae5c..0000000000 --- a/packages/go_router/lib/src/matching.dart +++ /dev/null @@ -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 pathParameters = {}; - final List matches = - _getLocRouteMatches(uri, extra, pathParameters); - return RouteMatchList( - matches: matches, uri: uri, pathParameters: pathParameters); - } - - List _getLocRouteMatches( - Uri uri, Object? extra, Map pathParameters) { - final List? 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 [], - uri: Uri(), - pathParameters: const {}); - - /// The route matches. - final List matches; - - /// Parameters for the matched route, URI-encoded. - /// - /// The parameters only reflects [RouteMatch]s that are not - /// [ImperativeRouteMatch]. - final Map 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 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: [...matches, match]); - } - - /// Returns a new instance of RouteMatchList with the input `match` removed - /// from the current instance. - RouteMatchList remove(RouteMatch match) { - final List 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 newParameters = []; - patternToRegExp(fullPath, newParameters); - final Set validParameters = newParameters.toSet(); - final Map newPathParameters = - Map.fromEntries( - pathParameters.entries.where((MapEntry 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 get routes => matches.map((RouteMatch e) => e.route).toList(); - - RouteMatchList _copyWith({ - List? matches, - Uri? uri, - Map? 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().equals(matches, other.matches) && - uri == other.uri && - const MapEquality() - .equals(pathParameters, other.pathParameters); - } - - @override - int get hashCode { - return Object.hash( - Object.hashAll(matches), - uri, - Object.hashAllUnordered( - pathParameters.entries.map((MapEntry 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> imperativeMatches = matchlist.matches - .whereType>() - .map((ImperativeRouteMatch e) => _toPrimitives( - e.matches.uri.toString(), e.extra, - pageKey: e.pageKey.value)) - .toList(); - - return { - _encodedDataKey: _toPrimitives( - matchlist.uri.toString(), matchlist.matches.first.extra, - imperativeMatches: imperativeMatches), - }; - } - - static Map _toPrimitives(String location, Object? state, - {List? imperativeMatches, String? pageKey}) { - return { - _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 data = - object[_encodedDataKey] as Map; - final Object? rootLocation = data[_locationKey]; - if (rootLocation is! String) { - return null; - } - final RouteMatchList matchList = - _matcher.findMatch(rootLocation, extra: data[_stateKey]); - - final List? imperativeMatches = - data[_imperativeMatchesKey] as List?; - 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 pageKey = - ValueKey(match[_pageKey] as String); - final RouteMatchList imperativeMatchList = _matcher.findMatch( - match[_locationKey] as String, - extra: match[_stateKey]); - final ImperativeRouteMatch imperativeMatch = - ImperativeRouteMatch( - 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? _getLocRouteRecursively({ - required String location, - required String remainingLocation, - required String matchedLocation, - required List routes, - required Map pathParameters, - required Object? extra, -}) { - List? result; - late Map subPathParameters; - // find the set of matches at this level of the tree - for (final RouteBase route in routes) { - subPathParameters = {}; - - 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 = [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? 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 = [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( - matchedLocation: uri.path, - extra: null, - error: error, - route: GoRoute( - path: uri.toString(), - pageBuilder: (BuildContext context, GoRouterState state) { - throw UnimplementedError(); - }, - ), - pageKey: const ValueKey('error'), - ), - ], - uri: uri, - pathParameters: const {}, - ); -} diff --git a/packages/go_router/lib/src/misc/errors.dart b/packages/go_router/lib/src/misc/errors.dart index 7aca0d8995..045e2ea04d 100644 --- a/packages/go_router/lib/src/misc/errors.dart +++ b/packages/go_router/lib/src/misc/errors.dart @@ -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 matches; + + @override + final String message; + + /// The location that was originally navigated to, before redirection began. + final Uri location; + + @override + String toString() => '${super.toString()} ${[ + ...matches + .map((RouteMatchList routeMatches) => routeMatches.uri.toString()), + ].join(' => ')}'; +} diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 78a7ee0624..17902413cf 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -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 { /// 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 { 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); + 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 [], - // 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 {}, - ); - } - Future processRedirectorResult(RouteMatchList matches) { - if (matches.isEmpty) { - return SynchronousFuture(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(matches); } - final FutureOr redirectorResult = redirector( + return debugParserFuture = _redirect( context, - SynchronousFuture(initialMatches), - configuration, - matcher, - extra: routeInformation.state, - ); - if (redirectorResult is RouteMatchList) { - return processRedirectorResult(redirectorResult); - } - - return debugParserFuture = redirectorResult.then(processRedirectorResult); + initialMatches, + ).then((RouteMatchList matchList) { + return _updateRouteMatchList( + matchList, + baseRouteMatchList: state.baseRouteMatchList, + completer: state.completer, + type: state.type, + ); + }); } @override @@ -128,16 +92,70 @@ class GoRouteInformationParser extends RouteInformationParser { 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).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 _redirect( + BuildContext context, RouteMatchList routeMatch) { + final FutureOr redirectedFuture = configuration + .redirect(context, routeMatch, redirectHistory: []); + if (redirectedFuture is RouteMatchList) { + return SynchronousFuture(redirectedFuture); + } + return redirectedFuture; + } + + RouteMatchList _updateRouteMatchList( + RouteMatchList newMatchList, { + required RouteMatchList? baseRouteMatchList, + required Completer? 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 _getUniqueValueKey() { + return ValueKey(String.fromCharCodes( + List.generate(32, (_) => _random.nextInt(33) + 89))); + } } diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart deleted file mode 100644 index 3ac4a5b9e6..0000000000 --- a/packages/go_router/lib/src/redirection.dart +++ /dev/null @@ -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 Function( - BuildContext, FutureOr, RouteConfiguration, RouteMatcher, - {List? redirectHistory, Object? extra}); - -/// Processes redirects by returning a new [RouteMatchList] representing the new -/// location. -FutureOr redirect( - BuildContext context, - FutureOr prevMatchListFuture, - RouteConfiguration configuration, - RouteMatcher matcher, - {List? redirectHistory, - Object? extra}) { - FutureOr processRedirect(RouteMatchList prevMatchList) { - final String prevLocation = prevMatchList.uri.toString(); - FutureOr 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 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 routeLevelRedirectResult = - _getRouteLevelRedirect(context, configuration, prevMatchList, 0); - if (routeLevelRedirectResult is String?) { - return processRouteLevelRedirect(routeLevelRedirectResult); - } - return routeLevelRedirectResult - .then(processRouteLevelRedirect); - } - - redirectHistory ??= [prevMatchList]; - // Check for top-level redirect - final FutureOr 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('topLevel'), - ), - ); - - if (topRedirectResult is String?) { - return processTopLevelRedirect(topRedirectResult); - } - return topRedirectResult.then(processTopLevelRedirect); - } - - if (prevMatchListFuture is RouteMatchList) { - return processRedirect(prevMatchListFuture); - } - return prevMatchListFuture.then(processRedirect); -} - -FutureOr _getRouteLevelRedirect( - BuildContext context, - RouteConfiguration configuration, - RouteMatchList matchList, - int currentCheckIndex, -) { - if (currentCheckIndex >= matchList.matches.length) { - return null; - } - final RouteMatch match = matchList.matches[currentCheckIndex]; - FutureOr processRouteRedirect(String? newLocation) => - newLocation ?? - _getRouteLevelRedirect( - context, configuration, matchList, currentCheckIndex + 1); - final RouteBase route = match.route; - FutureOr 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(processRouteRedirect); -} - -RouteMatchList _getNewMatches( - String newLocation, - Uri previousLocation, - RouteConfiguration configuration, - RouteMatcher matcher, - List 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 matches; - - @override - final String message; - - /// The location that was originally navigated to, before redirection began. - final Uri location; - - @override - String toString() => '${super.toString()} ${[ - ...matches - .map((RouteMatchList routeMatches) => routeMatches.uri.toString()), - ].join(' => ')}'; -} - -/// Adds the redirect to [redirects] if it is valid. -void _addRedirect(List 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', - [...redirects, newMatch], prevLocation); - } - if (redirects.length > redirectLimit) { - throw RedirectionError('too many redirects', - [...redirects, newMatch], prevLocation); - } - return true; - }()); - - redirects.add(newMatch); - - assert(() { - log.info('redirecting to $newMatch'); - return true; - }()); -} diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 80a885ed8a..8983a7894e 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -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 StatefulShellRoute get route => widget.route; GoRouter get _router => widget._router; - RouteMatcher get _matcher => _router.routeInformationParser.matcher; final Map _branchLocations = {}; @@ -1040,7 +1041,7 @@ class StatefulNavigationShellState extends State [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 if (index > 0) { final List 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 /// 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 /// [RestorableProperty] for enabling state restoration of [RouteMatchList]s. class _RestorableRouteMatchList extends RestorableProperty { - _RestorableRouteMatchList(RouteMatcher matcher) - : _matchListCodec = RouteMatchListCodec(matcher); + _RestorableRouteMatchList(RouteConfiguration configuration) + : _matchListCodec = RouteMatchListCodec(configuration); final RouteMatchListCodec _matchListCodec; @@ -1193,13 +1190,15 @@ class _RestorableRouteMatchList extends RestorableProperty { @override RouteMatchList fromPrimitives(Object? data) { - return _matchListCodec.decodeMatchList(data) ?? RouteMatchList.empty; + return data == null + ? RouteMatchList.empty + : _matchListCodec.decode(data as Map); } @override Object? toPrimitives() { if (value.isNotEmpty) { - return _matchListCodec.encodeMatchList(value); + return _matchListCodec.encode(value); } return null; } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 0e2d59b647..eac57c2255 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -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 { assert( initialExtra == null || initialLocation != null, 'initialLocation must be set in order to use initialExtra', - ) { + ), + assert(_debugCheckPath(routes, true)), + assert( + _debugVerifyNoDuplicatePathParameter(routes, {})), + assert(_debugCheckParentNavigatorKeys( + routes, + navigatorKey == null + ? >[] + : >[navigatorKey])) { setLogging(enabled: debugLogDiagnostics); WidgetsFlutterBinding.ensureInitialized(); navigatorKey ??= GlobalKey(); - _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 { 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 { }()); } - late final RouteConfiguration _routeConfiguration; - late final GoRouteInformationParser _routeInformationParser; - late final GoRouterDelegate _routerDelegate; - late final GoRouteInformationProvider _routeInformationProvider; + static bool _debugCheckPath(List 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 routes, List> allowedKeys) { + for (final RouteBase route in routes) { + if (route is GoRoute) { + final GlobalKey? 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, + >[ + // 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, + >[ + ...allowedKeys, + ], + ); + } + } else if (route is ShellRoute) { + _debugCheckParentNavigatorKeys( + route.routes, + >[...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, + >[ + ...allowedKeys, + branch.navigatorKey, + ], + ); + } + } + } + return true; + } + + static bool _debugVerifyNoDuplicatePathParameter( + List routes, Map 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 { /// 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 { 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 { routerDelegate.currentConfiguration.matches.last is ImperativeRouteMatch) { newLocation = (routerDelegate.currentConfiguration.matches.last - as ImperativeRouteMatch) + 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 { Map pathParameters = const {}, Map queryParameters = const {}, }) => - _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 { /// it as the same page. The page key will be reused. This will preserve the /// state and not run any page animation. Future push(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( + location, + base: routerDelegate.currentConfiguration, + extra: extra, ); - - return _routerDelegate.push(matches); } /// Push a named route onto the page stack w/ optional parameters, e.g. @@ -271,20 +371,14 @@ class GoRouter extends ChangeNotifier implements RouterConfig { /// * [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((RouteMatchList matchList) { - routerDelegate.pushReplacement(matchList); - }); + Future pushReplacement(String location, + {Object? extra}) { + log.info('pushReplacement $location'); + return routeInformationProvider.pushReplacement( + 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 { /// See also: /// * [goNamed] which navigates a named route. /// * [pushNamed] which pushes a named route onto the page stack. - void pushReplacementNamed( + Future pushReplacementNamed( String name, { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, }) { - pushReplacement( + return pushReplacement( namedLocation(name, pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, @@ -317,20 +411,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig { /// * [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((RouteMatchList matchList) { - routerDelegate.replace(matchList); - }); + Future replace(String location, {Object? extra}) { + log.info('replace $location'); + return routeInformationProvider.replace( + 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 { /// * [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 replaceNamed( String name, { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, }) { - replace( + return replace( namedLocation(name, pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, @@ -366,7 +453,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { log.info('popping $location'); return true; }()); - _routerDelegate.pop(result); + routerDelegate.pop(result); } /// Refresh the route. @@ -375,7 +462,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig { 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 { @override void dispose() { - _routeInformationProvider.dispose(); - _routerDelegate.removeListener(_handleStateMayChange); - _routerDelegate.dispose(); + routeInformationProvider.dispose(); + routerDelegate.removeListener(_handleStateMayChange); + routerDelegate.dispose(); super.dispose(); } diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index c360cef3d7..9c634eddbe 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -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 diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 7aae5cb12e..b71ea71440 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -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 diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart index ac97cab5dd..9d426992cb 100644 --- a/packages/go_router/test/builder_test.dart +++ b/packages/go_router/test/builder_test.dart @@ -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('/'), ), ], @@ -121,8 +118,6 @@ void main() { RouteMatch( route: config.routes.first as GoRoute, matchedLocation: '/', - extra: null, - error: null, pageKey: const ValueKey('/'), ), ], @@ -176,15 +171,11 @@ void main() { RouteMatch( route: config.routes.first, matchedLocation: '', - extra: null, - error: null, pageKey: const ValueKey(''), ), RouteMatch( route: config.routes.first.routes.first, matchedLocation: '/details', - extra: null, - error: null, pageKey: const ValueKey('/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('/a/details'), ), ], diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index 3124702218..ac6ff0e398 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -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()), ); }); @@ -318,7 +319,7 @@ void main() { }, ); }, - throwsAssertionError, + throwsA(isA()), ); }); @@ -373,7 +374,7 @@ void main() { }, ); }, - throwsAssertionError, + throwsA(isA()), ); }); @@ -749,7 +750,7 @@ void main() { return null; }, ), - throwsAssertionError, + throwsA(isA()), ); }, ); @@ -857,7 +858,7 @@ void main() { return null; }, ), - throwsAssertionError, + throwsA(isA()), ); }, ); diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart index c25877bf14..859a4f2d70 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -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('/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('/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) + (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('/a-p0'), - ); + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2); + final ValueKey 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('/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().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) + (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('/a-p0'), - ); + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2); + final ValueKey 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('/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('/a-p0'), - ); + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2); + final ValueKey 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('/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) + (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('/page-0-p0'), - ); + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2); + final ValueKey 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('/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('/page-0-p0'), - ); + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2); + final ValueKey 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('/page-0-p0'), + goRouter.routerDelegate.currentConfiguration.matches.last.pageKey, + prev, ); }, ); diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index cbdc543f46..b3b46660e6 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; - expect(matches, hasLength(1)); + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 log = []; 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 routes = [ + 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, [ + isMethodCall('selectMultiEntryHistory', arguments: null), + IsRouteUpdateCall('/settings', false, codec.encode(match.matches)), + ]); + GoRouter.optionURLReflectsImperativeAPIs = false; + }); + testWidgets('on push', (WidgetTester tester) async { final List routes = [ GoRoute( @@ -1004,10 +1051,13 @@ void main() { log.clear(); router.push('/settings'); + final RouteMatchListCodec codec = + RouteMatchListCodec(router.configuration); await tester.pumpAndSettle(); expect(log, [ 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, [ 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, [ 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, [ 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, [ 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, [ 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, [ 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 routes = [ + GoRoute( + path: '/', + builder: (_, __) => const DummyScreen(key: ValueKey('home')), + routes: [ + GoRoute( + path: 'settings', + builder: (_, GoRouterState state) => + DummyScreen(key: ValueKey('settings-${state.extra}')), + ), + ], + ), + ]; + + final GoRouter router = await createRouter(routes, tester); + expect(find.byKey(const ValueKey('home')), findsOneWidget); + + router.push('/settings', extra: 0); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('settings-0')), findsOneWidget); + + log.clear(); + router.push('/settings', extra: 1); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('settings-1')), findsOneWidget); + + final Map arguments = + log.last.arguments as Map; + // Stores the state after the last push. This should contain the encoded + // RouteMatchList. + final Object? state = + (log.last.arguments as Map)['state']; + final String location = + (arguments['location'] ?? arguments['uri']!) as String; + + router.go('/'); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('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('settings-1')), findsOneWidget); + + router.pop(); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('settings-0')), findsOneWidget); + + router.pop(); + await tester.pumpAndSettle(); + expect(find.byKey(const ValueKey('home')), findsOneWidget); + }); + testWidgets('works correctly with async redirect', (WidgetTester tester) async { final UniqueKey login = UniqueKey(); @@ -1198,10 +1319,13 @@ void main() { ), ]; final Completer completer = Completer(); - 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, [ 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: {'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 matches = router.routerDelegate.matches.matches; - expect(matches, hasLength(1)); + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); final TestErrorScreen screen = tester.widget(find.byType(TestErrorScreen)); @@ -1791,8 +1917,9 @@ void main() { tester, ); - final List matches = router.routerDelegate.matches.matches; - expect(matches, hasLength(1)); + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); final TestErrorScreen screen = tester.widget(find.byType(TestErrorScreen)); @@ -1813,8 +1940,9 @@ void main() { state.matchedLocation == '/' ? '/login' : null, ); - final List matches = router.routerDelegate.matches.matches; - expect(matches, hasLength(1)); + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); final TestErrorScreen screen = tester.widget(find.byType(TestErrorScreen)); @@ -1834,8 +1962,9 @@ void main() { : null, ); - final List matches = router.routerDelegate.matches.matches; - expect(matches, hasLength(1)); + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); final TestErrorScreen screen = tester.widget(find.byType(TestErrorScreen)); @@ -1895,7 +2024,8 @@ void main() { }, ); - final List matches = router.routerDelegate.matches.matches; + final List matches = + router.routerDelegate.currentConfiguration.matches; expect(matches, hasLength(1)); expect(find.byType(LoginScreen), findsOneWidget); }); @@ -1924,7 +2054,8 @@ void main() { initialLocation: loc, ); - final List matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; - expect(matches, hasLength(1)); + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(0)); expect(find.byType(TestErrorScreen), findsOneWidget); final TestErrorScreen screen = tester.widget(find.byType(TestErrorScreen)); expect(screen.ex, isNotNull); }); + testWidgets('can push error page', (WidgetTester tester) async { + final GoRouter router = await createRouter( + [ + 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 imperativeRouteMatch = - matches.matches.last as ImperativeRouteMatch; + 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': ['v2', 'v3'], }); await tester.pumpAndSettle(); - final List matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 matches = router.routerDelegate.matches.matches; + final List 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 diff --git a/packages/go_router/test/information_provider_test.dart b/packages/go_router/test/information_provider_test.dart index d5e2a0e891..d708515183 100644 --- a/packages/go_router/test/information_provider_test.dart +++ b/packages/go_router/test/information_provider_test.dart @@ -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)); }); }); } diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index 712f379d4d..f9e73075dd 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -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(); diff --git a/packages/go_router/test/matching_test.dart b/packages/go_router/test/matching_test.dart index 8a1d12ab53..7784b851d7 100644 --- a/packages/go_router/test/matching_test.dart +++ b/packages/go_router/test/matching_test.dart @@ -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 params2 = {}; @@ -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(), 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( - pageKey: const ValueKey('/b-p0'), matches: list2)); + final RouteMatchList list1 = configuration.findMatch('/a'); + final RouteMatchList list2 = configuration.findMatch('/b'); + list1.push(ImperativeRouteMatch( + pageKey: const ValueKey('/b-p0'), + matches: list2, + completer: Completer())); - final Object? encoded = codec.encodeMatchList(list1); - final RouteMatchList? decoded = codec.decodeMatchList(encoded); + final Map encoded = codec.encode(list1); + final RouteMatchList decoded = codec.decode(encoded); expect(decoded, isNotNull); expect(decoded, equals(list1)); diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart index 76a068b7d8..ee4279b515 100644 --- a/packages/go_router/test/parser_test.dart +++ b/packages/go_router/test/parser_test.dart @@ -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(type: NavigatingType.go, extra: extra)); } void main() { @@ -64,7 +67,7 @@ void main() { List 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 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 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 matches = matchesObj.matches; expect(matches, hasLength(2)); - expect(matches.first.error, isNull); + expect(matchesObj.error, isNull); }); } diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 19f795ceda..53838382bf 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -301,8 +301,6 @@ RouteMatch createRouteMatch(RouteBase route, String location) { return RouteMatch( route: route, matchedLocation: location, - extra: null, - error: null, pageKey: ValueKey(location), ); }