[go_router] Refactor RouterDelegate into functional pieces (#1653)
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index a37fabe..ea232e2 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,10 +1,14 @@
+## 4.0.0
+
+- Refactors go_router and introduces GoRouteInformationProvider. [Migration Doc](http://flutter.dev/go/go-router-v4-breaking-changes)
+
## 3.1.1
- Uses first match if there are more than one route to match. [ [#99833](https://github.com/flutter/flutter/issues/99833)
## 3.1.0
-- Added `GoRouteData` and `TypedGoRoute` to support `package:go_router_builder`.
+- Adds `GoRouteData` and `TypedGoRoute` to support `package:go_router_builder`.
## 3.0.7
diff --git a/packages/go_router/README.md b/packages/go_router/README.md
index 85f0712..83dbc20 100644
--- a/packages/go_router/README.md
+++ b/packages/go_router/README.md
@@ -15,6 +15,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: 'GoRouter Example',
diff --git a/packages/go_router/example/lib/async_data.dart b/packages/go_router/example/lib/async_data.dart
index 2a87a83..5dfa003 100644
--- a/packages/go_router/example/lib/async_data.dart
+++ b/packages/go_router/example/lib/async_data.dart
@@ -26,6 +26,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/books/main.dart b/packages/go_router/example/lib/books/main.dart
index 7f20594..241c368 100644
--- a/packages/go_router/example/lib/books/main.dart
+++ b/packages/go_router/example/lib/books/main.dart
@@ -31,6 +31,7 @@
Widget build(BuildContext context) => BookstoreAuthScope(
notifier: _auth,
child: MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routerDelegate: _router.routerDelegate,
routeInformationParser: _router.routeInformationParser,
),
diff --git a/packages/go_router/example/lib/cupertino.dart b/packages/go_router/example/lib/cupertino.dart
index 3511295..3f4bd10 100644
--- a/packages/go_router/example/lib/cupertino.dart
+++ b/packages/go_router/example/lib/cupertino.dart
@@ -17,6 +17,7 @@
@override
Widget build(BuildContext context) => CupertinoApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/error_screen.dart b/packages/go_router/example/lib/error_screen.dart
index 984441b..a578a83 100644
--- a/packages/go_router/example/lib/error_screen.dart
+++ b/packages/go_router/example/lib/error_screen.dart
@@ -17,6 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/extra_param.dart b/packages/go_router/example/lib/extra_param.dart
index 5e9141a..efab7be 100644
--- a/packages/go_router/example/lib/extra_param.dart
+++ b/packages/go_router/example/lib/extra_param.dart
@@ -27,6 +27,7 @@
home: NoExtraParamOnWebScreen(),
)
: MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/init_loc.dart b/packages/go_router/example/lib/init_loc.dart
index 4bef656..899c3c1 100644
--- a/packages/go_router/example/lib/init_loc.dart
+++ b/packages/go_router/example/lib/init_loc.dart
@@ -17,6 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/loading_page.dart b/packages/go_router/example/lib/loading_page.dart
index 2afb21e..9f61726 100644
--- a/packages/go_router/example/lib/loading_page.dart
+++ b/packages/go_router/example/lib/loading_page.dart
@@ -55,6 +55,7 @@
Widget build(BuildContext context) => ChangeNotifierProvider<AppState>.value(
value: _appState,
child: MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/main.dart b/packages/go_router/example/lib/main.dart
index ef33494..91b827b 100644
--- a/packages/go_router/example/lib/main.dart
+++ b/packages/go_router/example/lib/main.dart
@@ -17,6 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/named_routes.dart b/packages/go_router/example/lib/named_routes.dart
index 648610d..859a4bc 100644
--- a/packages/go_router/example/lib/named_routes.dart
+++ b/packages/go_router/example/lib/named_routes.dart
@@ -24,6 +24,7 @@
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: _loginInfo,
child: MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/nav_builder.dart b/packages/go_router/example/lib/nav_builder.dart
index ba8818f..8ec6b07 100644
--- a/packages/go_router/example/lib/nav_builder.dart
+++ b/packages/go_router/example/lib/nav_builder.dart
@@ -22,6 +22,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/nav_observer.dart b/packages/go_router/example/lib/nav_observer.dart
index 51cf45a..5afd016 100644
--- a/packages/go_router/example/lib/nav_observer.dart
+++ b/packages/go_router/example/lib/nav_observer.dart
@@ -18,6 +18,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/nested_nav.dart b/packages/go_router/example/lib/nested_nav.dart
index 5940f73..04b8285 100644
--- a/packages/go_router/example/lib/nested_nav.dart
+++ b/packages/go_router/example/lib/nested_nav.dart
@@ -19,6 +19,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/push.dart b/packages/go_router/example/lib/push.dart
index 9d24495..1288f12 100644
--- a/packages/go_router/example/lib/push.dart
+++ b/packages/go_router/example/lib/push.dart
@@ -17,6 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/query_params.dart b/packages/go_router/example/lib/query_params.dart
index 9a660ae..656273e 100644
--- a/packages/go_router/example/lib/query_params.dart
+++ b/packages/go_router/example/lib/query_params.dart
@@ -25,6 +25,7 @@
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: _loginInfo,
child: MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/redirection.dart b/packages/go_router/example/lib/redirection.dart
index 395e727..0cbe6ed 100644
--- a/packages/go_router/example/lib/redirection.dart
+++ b/packages/go_router/example/lib/redirection.dart
@@ -25,6 +25,7 @@
Widget build(BuildContext context) => ChangeNotifierProvider<LoginInfo>.value(
value: _loginInfo,
child: MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/router_neglect.dart b/packages/go_router/example/lib/router_neglect.dart
index 0e3ebac..4f4acfb 100644
--- a/packages/go_router/example/lib/router_neglect.dart
+++ b/packages/go_router/example/lib/router_neglect.dart
@@ -17,6 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/router_stream_refresh.dart b/packages/go_router/example/lib/router_stream_refresh.dart
index cb4607e..2d00dbe 100644
--- a/packages/go_router/example/lib/router_stream_refresh.dart
+++ b/packages/go_router/example/lib/router_stream_refresh.dart
@@ -94,6 +94,7 @@
Widget build(BuildContext context) => Provider<LoggedInState>.value(
value: loggedInState,
child: MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: App.title,
diff --git a/packages/go_router/example/lib/shared_scaffold.dart b/packages/go_router/example/lib/shared_scaffold.dart
index 8699f67..3dc0c10 100644
--- a/packages/go_router/example/lib/shared_scaffold.dart
+++ b/packages/go_router/example/lib/shared_scaffold.dart
@@ -19,6 +19,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/state_restoration.dart b/packages/go_router/example/lib/state_restoration.dart
index 695fa86..33d64ca 100644
--- a/packages/go_router/example/lib/state_restoration.dart
+++ b/packages/go_router/example/lib/state_restoration.dart
@@ -32,6 +32,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: App.title,
diff --git a/packages/go_router/example/lib/sub_routes.dart b/packages/go_router/example/lib/sub_routes.dart
index 204fc30..68288dd 100644
--- a/packages/go_router/example/lib/sub_routes.dart
+++ b/packages/go_router/example/lib/sub_routes.dart
@@ -19,6 +19,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/transitions.dart b/packages/go_router/example/lib/transitions.dart
index 2ccbcef..08c8faa 100644
--- a/packages/go_router/example/lib/transitions.dart
+++ b/packages/go_router/example/lib/transitions.dart
@@ -17,6 +17,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/url_strategy.dart b/packages/go_router/example/lib/url_strategy.dart
index f3b618a..0bebb1b 100644
--- a/packages/go_router/example/lib/url_strategy.dart
+++ b/packages/go_router/example/lib/url_strategy.dart
@@ -25,6 +25,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: App.title,
diff --git a/packages/go_router/example/lib/user_input.dart b/packages/go_router/example/lib/user_input.dart
index 3054cb7..56cf9a9 100644
--- a/packages/go_router/example/lib/user_input.dart
+++ b/packages/go_router/example/lib/user_input.dart
@@ -20,6 +20,7 @@
@override
Widget build(BuildContext context) => MaterialApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/example/lib/widgets_app.dart b/packages/go_router/example/lib/widgets_app.dart
index 6474f3c..cd4cc50 100644
--- a/packages/go_router/example/lib/widgets_app.dart
+++ b/packages/go_router/example/lib/widgets_app.dart
@@ -21,6 +21,7 @@
@override
Widget build(BuildContext context) => WidgetsApp.router(
+ routeInformationProvider: _router.routeInformationProvider,
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
title: title,
diff --git a/packages/go_router/lib/src/go_route_information_parser.dart b/packages/go_router/lib/src/go_route_information_parser.dart
index d5a7b15..04de89a 100644
--- a/packages/go_router/lib/src/go_route_information_parser.dart
+++ b/packages/go_router/lib/src/go_route_information_parser.dart
@@ -4,20 +4,435 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
+import 'package:go_router/src/go_route_information_provider.dart';
+
+import 'go_route.dart';
+import 'go_route_match.dart';
+import 'go_router_state.dart';
+import 'logging.dart';
+import 'path_parser.dart';
+import 'typedefs.dart';
+
+class _ParserError extends Error implements UnsupportedError {
+ _ParserError(this.message);
+
+ @override
+ final String? message;
+}
/// GoRouter implementation of the RouteInformationParser base class
-class GoRouteInformationParser extends RouteInformationParser<Uri> {
- /// for use by the Router architecture as part of the RouteInformationParser
- @override
- Future<Uri> parseRouteInformation(
- RouteInformation routeInformation,
- ) =>
- // Use [SynchronousFuture] so that the initial url is processed
- // synchronously and remove unwanted initial animations on deep-linking
- SynchronousFuture<Uri>(Uri.parse(routeInformation.location!));
+class GoRouteInformationParser
+ extends RouteInformationParser<List<GoRouteMatch>> {
+ /// Creates a [GoRouteInformationParser].
+ GoRouteInformationParser({
+ required this.routes,
+ required this.redirectLimit,
+ required this.topRedirect,
+ this.debugRequireGoRouteInformationProvider = false,
+ }) : assert(() {
+ // check top-level route paths are valid
+ for (final GoRoute route in routes) {
+ assert(route.path.startsWith('/'),
+ 'top-level path must start with "/": ${route.path}');
+ }
+ return true;
+ }()) {
+ _cacheNameToPath('', routes);
+ assert(() {
+ _debugLogKnownRoutes();
+ return true;
+ }());
+ }
+
+ /// List of top level routes used by the go router delegate.
+ final List<GoRoute> routes;
+
+ /// The limit for the number of consecutive redirects.
+ final int redirectLimit;
+
+ /// Top level page redirect.
+ final GoRouterRedirect topRedirect;
+
+ /// 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 in not in use.
+ ///
+ /// Defaults to false.
+ final bool debugRequireGoRouteInformationProvider;
+
+ final Map<String, String> _nameToPath = <String, String>{};
+
+ void _cacheNameToPath(String parentFullPath, List<GoRoute> childRoutes) {
+ for (final GoRoute route in childRoutes) {
+ final String fullPath = concatenatePaths(parentFullPath, route.path);
+
+ if (route.name != null) {
+ final String name = route.name!.toLowerCase();
+ assert(!_nameToPath.containsKey(name),
+ 'duplication fullpaths for name "$name":${_nameToPath[name]}, $fullPath');
+ _nameToPath[name] = fullPath;
+ }
+
+ if (route.routes.isNotEmpty) {
+ _cacheNameToPath(fullPath, route.routes);
+ }
+ }
+ }
+
+ /// Looks up the url location by a [GoRoute]'s name.
+ String namedLocation(
+ String name, {
+ Map<String, String> params = const <String, String>{},
+ Map<String, String> queryParams = const <String, String>{},
+ }) {
+ assert(() {
+ log.info('getting location for name: '
+ '"$name"'
+ '${params.isEmpty ? '' : ', params: $params'}'
+ '${queryParams.isEmpty ? '' : ', queryParams: $queryParams'}');
+ return true;
+ }());
+ assert(_nameToPath.containsKey(name), 'unknown route name: $name');
+ final String path = _nameToPath[name]!;
+ assert(() {
+ // Check that all required params are presented.
+ final List<String> paramNames = <String>[];
+ patternToRegExp(path, paramNames);
+ for (final String paramName in paramNames) {
+ assert(params.containsKey(paramName),
+ 'missing param "$paramName" for $path');
+ }
+
+ // Check that there are no extra params
+ for (final String key in params.keys) {
+ assert(paramNames.contains(key), 'unknown param "$key" for $path');
+ }
+ return true;
+ }());
+ final Map<String, String> encodedParams = <String, String>{
+ for (final MapEntry<String, String> param in params.entries)
+ param.key: Uri.encodeComponent(param.value)
+ };
+ final String location = patternToPath(path, encodedParams);
+ return Uri(path: location, queryParameters: queryParams).toString();
+ }
+
+ /// Concatenates two paths.
+ ///
+ /// e.g: pathA = /a, pathB = c/d, concatenatePaths(pathA, pathB) = /a/c/d.
+ static String concatenatePaths(String parentPath, String childPath) {
+ // at the root, just return the path
+ if (parentPath.isEmpty) {
+ assert(childPath.startsWith('/'));
+ assert(childPath == '/' || !childPath.endsWith('/'));
+ return childPath;
+ }
+
+ // not at the root, so append the parent path
+ assert(childPath.isNotEmpty);
+ assert(!childPath.startsWith('/'));
+ assert(!childPath.endsWith('/'));
+ return '${parentPath == '/' ? '' : parentPath}/$childPath';
+ }
/// for use by the Router architecture as part of the RouteInformationParser
@override
- RouteInformation restoreRouteInformation(Uri configuration) =>
- RouteInformation(location: configuration.toString());
+ Future<List<GoRouteMatch>> parseRouteInformation(
+ RouteInformation routeInformation,
+ ) {
+ assert(() {
+ if (debugRequireGoRouteInformationProvider) {
+ assert(
+ routeInformation is DebugGoRouteInformation,
+ 'This GoRouteInformationParser needs to be used with '
+ 'GoRouteInformationProvider, do you forget to pass in '
+ 'GoRouter.routeinformationProvider to the Router constructor?');
+ }
+ return true;
+ }());
+ final List<GoRouteMatch> matches =
+ _getLocRouteMatchesWithRedirects(routeInformation);
+ // Use [SynchronousFuture] so that the initial url is processed
+ // synchronously and remove unwanted initial animations on deep-linking
+ return SynchronousFuture<List<GoRouteMatch>>(matches);
+ }
+
+ List<GoRouteMatch> _getLocRouteMatchesWithRedirects(
+ RouteInformation routeInformation) {
+ // start redirecting from the initial location
+ List<GoRouteMatch> matches;
+ final String location = routeInformation.location!;
+ try {
+ // watch redirects for loops
+ final List<String> redirects = <String>[_canonicalUri(location)];
+ bool redirected(String? redir) {
+ if (redir == null) {
+ return false;
+ }
+
+ assert(() {
+ if (Uri.tryParse(redir) == null) {
+ throw _ParserError('invalid redirect: $redir');
+ }
+ if (redirects.contains(redir)) {
+ throw _ParserError('redirect loop detected: ${<String>[
+ ...redirects,
+ redir
+ ].join(' => ')}');
+ }
+ if (redirects.length > redirectLimit) {
+ throw _ParserError('too many redirects: ${<String>[
+ ...redirects,
+ redir
+ ].join(' => ')}');
+ }
+ return true;
+ }());
+
+ redirects.add(redir);
+ assert(() {
+ log.info('redirecting to $redir');
+ return true;
+ }());
+ return true;
+ }
+
+ // keep looping till we're done redirecting
+ while (true) {
+ final String loc = redirects.last;
+
+ // check for top-level redirect
+ final Uri uri = Uri.parse(loc);
+ if (redirected(
+ topRedirect(
+ GoRouterState(
+ this,
+ location: loc,
+ name: null, // no name available at the top level
+ // trim the query params off the subloc to match route.redirect
+ subloc: uri.path,
+ // pass along the query params 'cuz that's all we have right now
+ queryParams: uri.queryParameters,
+ extra: routeInformation.state,
+ ),
+ ),
+ )) {
+ continue;
+ }
+
+ // get stack of route matches
+ matches = _getLocRouteMatches(loc, routeInformation.state);
+
+ // merge new params to keep params from previously matched paths, e.g.
+ // /family/:fid/person/:pid provides fid and pid to person/:pid
+ Map<String, String> previouslyMatchedParams = <String, String>{};
+ for (final GoRouteMatch match in matches) {
+ assert(
+ !previouslyMatchedParams.keys.any(match.encodedParams.containsKey),
+ 'Duplicated parameter names',
+ );
+ match.encodedParams.addAll(previouslyMatchedParams);
+ previouslyMatchedParams = match.encodedParams;
+ }
+
+ // check top route for redirect
+ final GoRouteMatch top = matches.last;
+ if (redirected(
+ top.route.redirect(
+ GoRouterState(
+ this,
+ location: loc,
+ subloc: top.subloc,
+ name: top.route.name,
+ path: top.route.path,
+ fullpath: top.fullpath,
+ params: top.decodedParams,
+ queryParams: top.queryParams,
+ ),
+ ),
+ )) {
+ continue;
+ }
+
+ // no more redirects!
+ break;
+ }
+
+ // note that we need to catch it this way to get all the info, e.g. the
+ // file/line info for an error in an inline function impl, e.g. an inline
+ // `redirect` impl
+ // ignore: avoid_catches_without_on_clauses
+ } on _ParserError catch (err) {
+ // create a match that routes to the error page
+ final Exception error = Exception(err.message);
+ final Uri uri = Uri.parse(location);
+ matches = <GoRouteMatch>[
+ GoRouteMatch(
+ subloc: uri.path,
+ fullpath: uri.path,
+ encodedParams: <String, String>{},
+ queryParams: uri.queryParameters,
+ extra: null,
+ error: error,
+ route: GoRoute(
+ path: location,
+ pageBuilder: (BuildContext context, GoRouterState state) {
+ throw UnimplementedError();
+ }),
+ ),
+ ];
+ }
+ assert(matches.isNotEmpty);
+ return matches;
+ }
+
+ List<GoRouteMatch> _getLocRouteMatches(String location, Object? extra) {
+ final Uri uri = Uri.parse(location);
+ return _getLocRouteRecursively(
+ loc: uri.path,
+ restLoc: uri.path,
+ routes: routes,
+ parentFullpath: '',
+ parentSubloc: '',
+ queryParams: uri.queryParameters,
+ extra: extra,
+ );
+ }
+
+ static List<GoRouteMatch> _getLocRouteRecursively({
+ required String loc,
+ required String restLoc,
+ required String parentSubloc,
+ required List<GoRoute> routes,
+ required String parentFullpath,
+ required Map<String, String> queryParams,
+ required Object? extra,
+ }) {
+ bool debugGatherAllMatches = false;
+ assert(() {
+ debugGatherAllMatches = true;
+ return true;
+ }());
+ final List<List<GoRouteMatch>> result = <List<GoRouteMatch>>[];
+ // find the set of matches at this level of the tree
+ for (final GoRoute route in routes) {
+ final String fullpath = concatenatePaths(parentFullpath, route.path);
+ final GoRouteMatch? match = GoRouteMatch.match(
+ route: route,
+ restLoc: restLoc,
+ parentSubloc: parentSubloc,
+ fullpath: fullpath,
+ queryParams: queryParams,
+ extra: extra,
+ );
+
+ if (match == null) {
+ continue;
+ }
+ if (match.subloc.toLowerCase() == loc.toLowerCase()) {
+ // If it is a complete match, then return the matched route
+ // NOTE: need a lower case match because subloc is canonicalized to match
+ // the path case whereas the location can be of any case and still match
+ result.add(<GoRouteMatch>[match]);
+ } else if (route.routes.isEmpty) {
+ // If it is partial match but no sub-routes, bail.
+ continue;
+ } else {
+ // otherwise recurse
+ final String childRestLoc =
+ loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1));
+ assert(loc.startsWith(match.subloc));
+ assert(restLoc.isNotEmpty);
+
+ final List<GoRouteMatch> subRouteMatch = _getLocRouteRecursively(
+ loc: loc,
+ restLoc: childRestLoc,
+ parentSubloc: match.subloc,
+ routes: route.routes,
+ parentFullpath: fullpath,
+ queryParams: queryParams,
+ extra: extra,
+ ).toList();
+
+ // if there's no sub-route matches, there is no match for this
+ // location
+ if (subRouteMatch.isEmpty) {
+ continue;
+ }
+ result.add(<GoRouteMatch>[match, ...subRouteMatch]);
+ }
+ // Should only reach here if there is a match.
+ if (debugGatherAllMatches) {
+ continue;
+ } else {
+ break;
+ }
+ }
+
+ if (result.isEmpty) {
+ throw _ParserError('no routes for location: $loc');
+ }
+
+ // If there are multiple routes that match the location, returning the first one.
+ // To make predefined routes to take precedence over dynamic routes eg. '/:id'
+ // consider adding the dynamic route at the end of the routes
+ return result.first;
+ }
+
+ void _debugLogKnownRoutes() {
+ log.info('known full paths for routes:');
+ _debugLogFullPathsFor(routes, '', 0);
+
+ if (_nameToPath.isNotEmpty) {
+ log.info('known full paths for route names:');
+ for (final MapEntry<String, String> e in _nameToPath.entries) {
+ log.info(' ${e.key} => ${e.value}');
+ }
+ }
+ }
+
+ void _debugLogFullPathsFor(
+ List<GoRoute> routes,
+ String parentFullpath,
+ int depth,
+ ) {
+ for (final GoRoute route in routes) {
+ final String fullpath = concatenatePaths(parentFullpath, route.path);
+ assert(() {
+ log.info(' => ${''.padLeft(depth * 2)}$fullpath');
+ return true;
+ }());
+ _debugLogFullPathsFor(route.routes, fullpath, depth + 1);
+ }
+ }
+
+ /// for use by the Router architecture as part of the RouteInformationParser
+ @override
+ RouteInformation restoreRouteInformation(List<GoRouteMatch> configuration) {
+ return RouteInformation(
+ location: configuration.last.fullUriString,
+ state: configuration.last.extra);
+ }
+}
+
+/// Normalizes the location string.
+String _canonicalUri(String loc) {
+ String canon = Uri.parse(loc).toString();
+ canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon;
+
+ // remove trailing slash except for when you shouldn't, e.g.
+ // /profile/ => /profile
+ // / => /
+ // /login?from=/ => login?from=/
+ canon = canon.endsWith('/') && canon != '/' && !canon.contains('?')
+ ? canon.substring(0, canon.length - 1)
+ : canon;
+
+ // /login/?from=/ => /login?from=/
+ // /?from=/ => /?from=/
+ canon = canon.replaceFirst('/?', '?', 1);
+
+ return canon;
}
diff --git a/packages/go_router/lib/src/go_route_information_provider.dart b/packages/go_router/lib/src/go_route_information_provider.dart
new file mode 100644
index 0000000..549f6b6
--- /dev/null
+++ b/packages/go_router/lib/src/go_route_information_provider.dart
@@ -0,0 +1,120 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:go_router/src/go_route_information_parser.dart';
+
+/// The route information provider created by go_router
+class GoRouteInformationProvider extends RouteInformationProvider
+ with WidgetsBindingObserver, ChangeNotifier {
+ /// Creates a [GoRouteInformationProvider].
+ GoRouteInformationProvider({
+ required RouteInformation initialRouteInformation,
+ Listenable? refreshListenable,
+ }) : _refreshListenable = refreshListenable,
+ _value = initialRouteInformation {
+ _refreshListenable?.addListener(notifyListeners);
+ }
+
+ final Listenable? _refreshListenable;
+
+ // ignore: unnecessary_non_null_assertion
+ static WidgetsBinding get _binding => WidgetsBinding.instance!;
+
+ @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 &&
+ _valueInEngine.location == routeInformation.location);
+ SystemNavigator.selectMultiEntryHistory();
+ // TODO(chunhtai): should report extra to to browser through state if
+ // possible.
+ SystemNavigator.routeInformationUpdated(
+ location: routeInformation.location!,
+ replace: replace,
+ );
+ _value = routeInformation;
+ _valueInEngine = routeInformation;
+ }
+
+ @override
+ RouteInformation get value => DebugGoRouteInformation(
+ location: _value.location,
+ state: _value.state,
+ );
+ RouteInformation _value;
+ set value(RouteInformation other) {
+ final bool shouldNotify =
+ value.location != other.location || value.state != other.state;
+ _value = other;
+ if (shouldNotify) {
+ notifyListeners();
+ }
+ }
+
+ RouteInformation _valueInEngine =
+ RouteInformation(location: _binding.platformDispatcher.defaultRouteName);
+
+ void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
+ if (_value == routeInformation) {
+ return;
+ }
+ _value = routeInformation;
+ _valueInEngine = routeInformation;
+ notifyListeners();
+ }
+
+ @override
+ void addListener(VoidCallback listener) {
+ if (!hasListeners) {
+ _binding.addObserver(this);
+ }
+ super.addListener(listener);
+ }
+
+ @override
+ void removeListener(VoidCallback listener) {
+ super.removeListener(listener);
+ if (!hasListeners) {
+ _binding.removeObserver(this);
+ }
+ }
+
+ @override
+ void dispose() {
+ if (hasListeners) {
+ _binding.removeObserver(this);
+ }
+ _refreshListenable?.removeListener(notifyListeners);
+ super.dispose();
+ }
+
+ @override
+ Future<bool> didPushRouteInformation(
+ RouteInformation routeInformation) async {
+ assert(hasListeners);
+ print('_platformReportsNewRouteInformation $routeInformation');
+ _platformReportsNewRouteInformation(routeInformation);
+ return true;
+ }
+
+ @override
+ Future<bool> didPushRoute(String route) async {
+ assert(hasListeners);
+ _platformReportsNewRouteInformation(RouteInformation(location: route));
+ return true;
+ }
+}
+
+/// A debug class that is used for asserting the [GoRouteInformationProvider] is
+/// in use with the [GoRouteInformationParser].
+class DebugGoRouteInformation extends RouteInformation {
+ /// Creates
+ DebugGoRouteInformation({String? location, Object? state})
+ : super(location: location, state: state);
+}
diff --git a/packages/go_router/lib/src/go_route_match.dart b/packages/go_router/lib/src/go_route_match.dart
index 345aa5d..35a03aa 100644
--- a/packages/go_router/lib/src/go_route_match.dart
+++ b/packages/go_router/lib/src/go_route_match.dart
@@ -5,7 +5,7 @@
import 'package:flutter/foundation.dart';
import 'go_route.dart';
-import 'go_router_delegate.dart';
+import 'go_route_information_parser.dart';
import 'path_parser.dart';
/// Each GoRouteMatch instance represents an instance of a GoRoute for a
@@ -22,7 +22,8 @@
required this.extra,
required this.error,
this.pageKey,
- }) : assert(subloc.startsWith('/')),
+ }) : fullUriString = _addQueryParams(subloc, queryParams),
+ assert(subloc.startsWith('/')),
assert(Uri.parse(subloc).queryParameters.isEmpty),
assert(fullpath.startsWith('/')),
assert(Uri.parse(fullpath).queryParameters.isEmpty),
@@ -35,60 +36,15 @@
}());
// ignore: public_member_api_docs
- factory GoRouteMatch.matchNamed({
- required GoRoute route,
- required String name, // e.g. person
- required String fullpath, // e.g. /family/:fid/person/:pid
- required Map<String, String> params, // e.g. {'fid': 'f2', 'pid': 'p1'}
- required Map<String, String> queryParams, // e.g. {'from': '/family/f2'}
- required Object? extra,
- }) {
- assert(route.name != null);
- assert(route.name!.toLowerCase() == name.toLowerCase());
- assert(() {
- // check that we have all the params we need
- final List<String> paramNames = <String>[];
- patternToRegExp(fullpath, paramNames);
- for (final String paramName in paramNames) {
- assert(params.containsKey(paramName),
- 'missing param "$paramName" for $fullpath');
- }
-
- // check that we have don't have extra params
- for (final String key in params.keys) {
- assert(paramNames.contains(key), 'unknown param "$key" for $fullpath');
- }
- return true;
- }());
-
- final Map<String, String> encodedParams = <String, String>{
- for (final MapEntry<String, String> param in params.entries)
- param.key: Uri.encodeComponent(param.value)
- };
-
- final String subloc = _locationFor(fullpath, encodedParams);
- return GoRouteMatch(
- route: route,
- subloc: subloc,
- fullpath: fullpath,
- encodedParams: encodedParams,
- queryParams: queryParams,
- extra: extra,
- error: null,
- );
- }
-
- // ignore: public_member_api_docs
static GoRouteMatch? match({
required GoRoute route,
required String restLoc, // e.g. person/p1
required String parentSubloc, // e.g. /family/f2
- required String path, // e.g. person/:pid
required String fullpath, // e.g. /family/:fid/person/:pid
required Map<String, String> queryParams,
required Object? extra,
}) {
- assert(!path.contains('//'));
+ assert(!route.path.contains('//'));
final RegExpMatch? match = route.matchPatternAsPrefix(restLoc);
if (match == null) {
@@ -96,8 +52,9 @@
}
final Map<String, String> encodedParams = route.extractPathParams(match);
- final String pathLoc = _locationFor(path, encodedParams);
- final String subloc = GoRouterDelegate.fullLocFor(parentSubloc, pathLoc);
+ final String pathLoc = patternToPath(route.path, encodedParams);
+ final String subloc =
+ GoRouteInformationParser.concatenatePaths(parentSubloc, pathLoc);
return GoRouteMatch(
route: route,
subloc: subloc,
@@ -133,6 +90,18 @@
/// Optional value key of type string, to hold a unique reference to a page.
final ValueKey<String>? pageKey;
+ /// The full uri string
+ final String fullUriString; // e.g. /family/12?query=14
+
+ static String _addQueryParams(String loc, Map<String, String> queryParams) {
+ final Uri uri = Uri.parse(loc);
+ assert(uri.queryParameters.isEmpty);
+ return Uri(
+ path: uri.path,
+ queryParameters: queryParams.isEmpty ? null : queryParams)
+ .toString();
+ }
+
/// Parameters for the matched route, URI-decoded.
Map<String, String> get decodedParams => <String, String>{
for (final MapEntry<String, String> param in encodedParams.entries)
@@ -142,8 +111,4 @@
/// for use by the Router architecture as part of the GoRouteMatch
@override
String toString() => 'GoRouteMatch($fullpath, $encodedParams)';
-
- /// expand a path w/ param slots using params, e.g. family/:fid => family/f1
- static String _locationFor(String pattern, Map<String, String> params) =>
- patternToPath(pattern, params);
}
diff --git a/packages/go_router/lib/src/go_router.dart b/packages/go_router/lib/src/go_router.dart
index 7ed7425..b3e4c3f 100644
--- a/packages/go_router/lib/src/go_router.dart
+++ b/packages/go_router/lib/src/go_router.dart
@@ -6,6 +6,8 @@
import 'go_route.dart';
import 'go_route_information_parser.dart';
+import 'go_route_information_provider.dart';
+import 'go_route_match.dart';
import 'go_router_delegate.dart';
import 'go_router_state.dart';
import 'inherited_go_router.dart';
@@ -30,7 +32,7 @@
Listenable? refreshListenable,
int redirectLimit = 5,
bool routerNeglect = false,
- String initialLocation = '/',
+ String? initialLocation,
UrlPathStrategy? urlPathStrategy,
List<NavigatorObserver>? observers,
bool debugLogDiagnostics = false,
@@ -42,21 +44,31 @@
}
setLogging(enabled: debugLogDiagnostics);
+ WidgetsFlutterBinding.ensureInitialized();
- routerDelegate = GoRouterDelegate(
+ final String _effectiveInitialLocation = initialLocation ??
+ // ignore: unnecessary_non_null_assertion
+ WidgetsBinding.instance!.platformDispatcher.defaultRouteName;
+ routeInformationParser = GoRouteInformationParser(
routes: routes,
- errorPageBuilder: errorPageBuilder,
- errorBuilder: errorBuilder,
topRedirect: redirect ?? (_) => null,
redirectLimit: redirectLimit,
- refreshListenable: refreshListenable,
+ debugRequireGoRouteInformationProvider: true,
+ );
+ routeInformationProvider = GoRouteInformationProvider(
+ initialRouteInformation:
+ RouteInformation(location: _effectiveInitialLocation),
+ refreshListenable: refreshListenable);
+
+ routerDelegate = GoRouterDelegate(
+ routeInformationParser,
+ errorPageBuilder: errorPageBuilder,
+ errorBuilder: errorBuilder,
routerNeglect: routerNeglect,
- initUri: Uri.parse(initialLocation),
observers: <NavigatorObserver>[
...observers ?? <NavigatorObserver>[],
this
],
- debugLogDiagnostics: debugLogDiagnostics,
restorationScopeId: restorationScopeId,
// wrap the returned Navigator to enable GoRouter.of(context).go() et al,
// allowing the caller to wrap the navigator themselves
@@ -67,17 +79,23 @@
child: navigatorBuilder?.call(context, state, nav) ?? nav,
),
);
+ assert(() {
+ log.info('setting initial location $initialLocation');
+ return true;
+ }());
}
/// The route information parser used by the go router.
- final GoRouteInformationParser routeInformationParser =
- GoRouteInformationParser();
+ late final GoRouteInformationParser routeInformationParser;
/// The router delegate used by the go router.
late final GoRouterDelegate routerDelegate;
+ /// The route information provider used by the go router.
+ late final GoRouteInformationProvider routeInformationProvider;
+
/// Get the current location.
- String get location => routerDelegate.currentConfiguration.toString();
+ String get location => routerDelegate.currentConfiguration.last.fullUriString;
/// Get a location from route name and parameters.
/// This is useful for redirecting to a named location.
@@ -86,7 +104,7 @@
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
}) =>
- routerDelegate.namedLocation(
+ routeInformationParser.namedLocation(
name,
params: params,
queryParams: queryParams,
@@ -94,8 +112,14 @@
/// Navigate to a URI location w/ optional query parameters, e.g.
/// `/family/f2/person/p1?color=blue`
- void go(String location, {Object? extra}) =>
- routerDelegate.go(location, extra: extra);
+ void go(String location, {Object? extra}) {
+ assert(() {
+ log.info('going to $location');
+ return true;
+ }());
+ routeInformationProvider.value =
+ RouteInformation(location: location, state: extra);
+ }
/// Navigate to a named route w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
@@ -113,8 +137,18 @@
/// Push a URI location onto the page stack w/ optional query parameters, e.g.
/// `/family/f2/person/p1?color=blue`
- void push(String location, {Object? extra}) =>
- routerDelegate.push(location, extra: extra);
+ void push(String location, {Object? extra}) {
+ assert(() {
+ log.info('pushing $location');
+ return true;
+ }());
+ routeInformationParser
+ .parseRouteInformation(
+ DebugGoRouteInformation(location: location, state: extra))
+ .then<void>((List<GoRouteMatch> matches) {
+ routerDelegate.push(matches.last);
+ });
+ }
/// Push a named route onto the page stack w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
@@ -133,7 +167,13 @@
void pop() => routerDelegate.pop();
/// Refresh the route.
- void refresh() => routerDelegate.refresh();
+ void refresh() {
+ assert(() {
+ log.info('refreshing $location');
+ return true;
+ }());
+ routeInformationProvider.notifyListeners();
+ }
/// Set the app's URL path strategy (defaults to hash). call before runApp().
static void setUrlPathStrategy(UrlPathStrategy strategy) =>
@@ -166,4 +206,11 @@
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
notifyListeners();
+
+ @override
+ void dispose() {
+ routeInformationProvider.dispose();
+ routerDelegate.dispose();
+ super.dispose();
+ }
}
diff --git a/packages/go_router/lib/src/go_router_delegate.dart b/packages/go_router/lib/src/go_router_delegate.dart
index f901a94..fed213c 100644
--- a/packages/go_router/lib/src/go_router_delegate.dart
+++ b/packages/go_router/lib/src/go_router_delegate.dart
@@ -8,7 +8,7 @@
import 'package:flutter/widgets.dart';
import 'custom_transition_page.dart';
-import 'go_route.dart';
+import 'go_route_information_parser.dart';
import 'go_route_match.dart';
import 'go_router_cupertino.dart';
import 'go_router_error_page.dart';
@@ -19,76 +19,36 @@
import 'typedefs.dart';
/// GoRouter implementation of the RouterDelegate base class.
-class GoRouterDelegate extends RouterDelegate<Uri>
- with
- PopNavigatorRouterDelegateMixin<Uri>,
- // ignore: prefer_mixin
- ChangeNotifier {
+class GoRouterDelegate extends RouterDelegate<List<GoRouteMatch>>
+ with PopNavigatorRouterDelegateMixin<List<GoRouteMatch>>, ChangeNotifier {
/// Constructor for GoRouter's implementation of the
/// RouterDelegate base class.
- GoRouterDelegate({
+ GoRouterDelegate(
+ this._parser, {
required this.builderWithNav,
- required this.routes,
required this.errorPageBuilder,
required this.errorBuilder,
- required this.topRedirect,
- required this.redirectLimit,
- required this.refreshListenable,
- required Uri initUri,
required this.observers,
- required this.debugLogDiagnostics,
required this.routerNeglect,
this.restorationScopeId,
- }) : assert(() {
- // check top-level route paths are valid
- for (final GoRoute route in routes) {
- assert(route.path.startsWith('/'),
- 'top-level path must start with "/": ${route.path}');
- }
- return true;
- }()) {
- // cache the set of named routes for fast lookup
- _cacheNamedRoutes(routes, '', _namedMatches);
+ });
- // output known routes
- _outputKnownRoutes();
-
- // build the list of route matches
- log.info('setting initial location $initUri');
- _go(initUri.toString());
-
- // when the listener changes, refresh the route
- refreshListenable?.addListener(refresh);
- }
+ // TODO(chunhtai): remove this once namedLocation is removed from go_router.
+ final GoRouteInformationParser _parser;
/// Builder function for a go router with Navigator.
final GoRouterBuilderWithNav builderWithNav;
- /// List of top level routes used by the go router delegate.
- final List<GoRoute> routes;
-
/// Error page builder for the go router delegate.
final GoRouterPageBuilder? errorPageBuilder;
/// Error widget builder for the go router delegate.
final GoRouterWidgetBuilder? errorBuilder;
- /// Top level page redirect.
- final GoRouterRedirect topRedirect;
-
- /// The limit for the number of consecutive redirects.
- final int redirectLimit;
-
- /// Listenable used to cause the router to refresh it's route.
- final Listenable? refreshListenable;
-
/// NavigatorObserver used to receive change notifications when
/// navigation changes.
final List<NavigatorObserver> observers;
- /// Set to true to log diagnostic info for your routes.
- final bool debugLogDiagnostics;
-
/// Set to true to disable creating history entries on the web.
final bool routerNeglect;
@@ -97,78 +57,11 @@
final String? restorationScopeId;
final GlobalKey<NavigatorState> _key = GlobalKey<NavigatorState>();
- final List<GoRouteMatch> _matches = <GoRouteMatch>[];
- final Map<String, GoRouteMatch> _namedMatches = <String, GoRouteMatch>{};
- final Map<String, int> _pushCounts = <String, int>{};
-
- void _cacheNamedRoutes(
- List<GoRoute> routes,
- String parentFullpath,
- Map<String, GoRouteMatch> namedFullpaths,
- ) {
- for (final GoRoute route in routes) {
- final String fullpath = fullLocFor(parentFullpath, route.path);
-
- if (route.name != null) {
- final String name = route.name!.toLowerCase();
- assert(!namedFullpaths.containsKey(name),
- 'duplication fullpaths for name "$name":${namedFullpaths[name]!.fullpath}, $fullpath');
-
- // we only have a partial match until we have a location;
- // we're really only caching the route and fullpath at this point
- final GoRouteMatch match = GoRouteMatch(
- route: route,
- subloc: '/TBD',
- fullpath: fullpath,
- encodedParams: <String, String>{},
- queryParams: <String, String>{},
- extra: null,
- error: null,
- );
-
- namedFullpaths[name] = match;
- }
-
- if (route.routes.isNotEmpty) {
- _cacheNamedRoutes(route.routes, fullpath, namedFullpaths);
- }
- }
- }
-
- /// Get a location from route name and parameters.
- /// This is useful for redirecting to a named location.
- String namedLocation(
- String name, {
- required Map<String, String> params,
- required Map<String, String> queryParams,
- }) {
- log.info('getting location for name: '
- '"$name"'
- '${params.isEmpty ? '' : ', params: $params'}'
- '${queryParams.isEmpty ? '' : ', queryParams: $queryParams'}');
-
- // find route and build up the full path along the way
- final GoRouteMatch? match = _getNameRouteMatch(
- name.toLowerCase(), // case-insensitive name matching
- params: params,
- queryParams: queryParams,
- );
- assert(match != null, 'unknown route name: $name');
- assert(identical(match!.queryParams, queryParams));
- return _addQueryParams(match!.subloc, queryParams);
- }
-
- /// Navigate to the given location.
- void go(String location, {Object? extra}) {
- log.info('going to $location');
- _go(location, extra: extra);
- notifyListeners();
- }
+ List<GoRouteMatch> _matches = const <GoRouteMatch>[];
/// Push the given location onto the page stack
- void push(String location, {Object? extra}) {
- log.info('pushing $location');
- _push(location, extra: extra);
+ void push(GoRouteMatch match) {
+ _matches.add(match);
notifyListeners();
}
@@ -180,35 +73,17 @@
notifyListeners();
}
- /// Refresh the current location, including re-evaluating redirections.
- void refresh() {
- log.info('refreshing $location');
- _go(location, extra: _matches.last.extra);
- notifyListeners();
- }
-
- /// Get the current location, e.g. /family/f2/person/p1
- String get location =>
- _addQueryParams(_matches.last.subloc, _matches.last.queryParams);
-
/// For internal use; visible for testing only.
@visibleForTesting
List<GoRouteMatch> get matches => _matches;
- /// Dispose resources held by the router delegate.
- @override
- void dispose() {
- refreshListenable?.removeListener(refresh);
- super.dispose();
- }
-
/// For use by the Router architecture as part of the RouterDelegate.
@override
GlobalKey<NavigatorState> get navigatorKey => _key;
/// For use by the Router architecture as part of the RouterDelegate.
@override
- Uri get currentConfiguration => Uri.parse(location);
+ List<GoRouteMatch> get currentConfiguration => _matches;
/// For use by the Router architecture as part of the RouterDelegate.
@override
@@ -216,394 +91,17 @@
/// For use by the Router architecture as part of the RouterDelegate.
@override
- Future<void> setInitialRoutePath(Uri configuration) {
- // if the initial location is /, then use the dev initial location;
- // otherwise, we're cruising to a deep link, so ignore dev initial location
- final String config = configuration.toString();
- if (config == '/') {
- _go(location);
- } else {
- log.info('deep linking to $config');
- _go(config);
- }
-
+ Future<void> setNewRoutePath(List<GoRouteMatch> configuration) {
+ _matches = configuration;
// Use [SynchronousFuture] so that the initial url is processed
// synchronously and remove unwanted initial animations on deep-linking
return SynchronousFuture<void>(null);
}
- /// For use by the Router architecture as part of the RouterDelegate.
- @override
- Future<void> setNewRoutePath(Uri configuration) async {
- final String config = configuration.toString();
- log.info('going to $config');
- _go(config);
- }
-
- void _go(String location, {Object? extra}) {
- final List<GoRouteMatch> matches =
- _getLocRouteMatchesWithRedirects(location, extra: extra);
- assert(matches.isNotEmpty);
-
- // replace the stack of matches w/ the new ones
- _matches
- ..clear()
- ..addAll(matches);
- }
-
- void _push(String location, {Object? extra}) {
- final List<GoRouteMatch> matches =
- _getLocRouteMatchesWithRedirects(location, extra: extra);
- assert(matches.isNotEmpty);
- final GoRouteMatch top = matches.last;
-
- // remap the pageKey so allow any number of the same page on the stack
- final String fullpath = top.fullpath;
- final int count = (_pushCounts[fullpath] ?? 0) + 1;
- _pushCounts[fullpath] = count;
- final ValueKey<String> pageKey = ValueKey<String>('$fullpath-p$count');
- final GoRouteMatch match = GoRouteMatch(
- route: top.route,
- subloc: top.subloc,
- fullpath: top.fullpath,
- encodedParams: top.encodedParams,
- queryParams: top.queryParams,
- extra: extra,
- error: null,
- pageKey: pageKey,
- );
-
- // add a new match onto the stack of matches
- assert(matches.isNotEmpty);
- _matches.add(match);
- }
-
- List<GoRouteMatch> _getLocRouteMatchesWithRedirects(
- String location, {
- required Object? extra,
- }) {
- // start redirecting from the initial location
- List<GoRouteMatch> matches;
-
- try {
- // watch redirects for loops
- final List<String> redirects = <String>[_canonicalUri(location)];
- bool redirected(String? redir) {
- if (redir == null) {
- return false;
- }
-
- assert(Uri.tryParse(redir) != null, 'invalid redirect: $redir');
-
- assert(
- !redirects.contains(redir),
- 'redirect loop detected: ${<String>[
- ...redirects,
- redir
- ].join(' => ')}');
- assert(
- redirects.length < redirectLimit,
- 'too many redirects: ${<String>[
- ...redirects,
- redir
- ].join(' => ')}');
-
- redirects.add(redir);
- log.info('redirecting to $redir');
- return true;
- }
-
- // keep looping till we're done redirecting
- while (true) {
- final String loc = redirects.last;
-
- // check for top-level redirect
- final Uri uri = Uri.parse(loc);
- if (redirected(
- topRedirect(
- GoRouterState(
- this,
- location: loc,
- name: null, // no name available at the top level
- // trim the query params off the subloc to match route.redirect
- subloc: uri.path,
- // pass along the query params 'cuz that's all we have right now
- queryParams: uri.queryParameters,
- ),
- ),
- )) {
- continue;
- }
-
- // get stack of route matches
- matches = _getLocRouteMatches(loc, extra: extra);
-
- // merge new params to keep params from previously matched paths, e.g.
- // /family/:fid/person/:pid provides fid and pid to person/:pid
- Map<String, String> previouslyMatchedParams = <String, String>{};
- for (final GoRouteMatch match in matches) {
- assert(
- !previouslyMatchedParams.keys.any(match.encodedParams.containsKey),
- 'Duplicated parameter names',
- );
- match.encodedParams.addAll(previouslyMatchedParams);
- previouslyMatchedParams = match.encodedParams;
- }
-
- // check top route for redirect
- final GoRouteMatch top = matches.last;
- if (redirected(
- top.route.redirect(
- GoRouterState(
- this,
- location: loc,
- subloc: top.subloc,
- name: top.route.name,
- path: top.route.path,
- fullpath: top.fullpath,
- params: top.decodedParams,
- queryParams: top.queryParams,
- extra: extra,
- ),
- ),
- )) {
- continue;
- }
-
- // let Router know to update the address bar
- // (the initial route is not a redirect)
- if (redirects.length > 1) {
- notifyListeners();
- }
-
- // no more redirects!
- break;
- }
-
- // note that we need to catch it this way to get all the info, e.g. the
- // file/line info for an error in an inline function impl, e.g. an inline
- // `redirect` impl
- // ignore: avoid_catches_without_on_clauses
- } catch (err, stack) {
- log.severe('Exception during GoRouter navigation', err, stack);
-
- // create a match that routes to the error page
- final Exception error = err is Exception ? err : Exception(err);
- final Uri uri = Uri.parse(location);
- matches = <GoRouteMatch>[
- GoRouteMatch(
- subloc: uri.path,
- fullpath: uri.path,
- encodedParams: <String, String>{},
- queryParams: uri.queryParameters,
- extra: null,
- error: error,
- route: GoRoute(
- path: location,
- pageBuilder: (BuildContext context, GoRouterState state) =>
- _errorPageBuilder(
- context,
- GoRouterState(
- this,
- location: state.location,
- subloc: state.subloc,
- name: state.name,
- path: state.path,
- error: error,
- fullpath: state.path,
- params: state.params,
- queryParams: state.queryParams,
- extra: state.extra,
- ),
- ),
- ),
- ),
- ];
- }
-
- assert(matches.isNotEmpty);
- return matches;
- }
-
- List<GoRouteMatch> _getLocRouteMatches(
- String location, {
- Object? extra,
- }) {
- final Uri uri = Uri.parse(location);
- final List<List<GoRouteMatch>> matchStacks = _getLocRouteMatchStacks(
- loc: uri.path,
- restLoc: uri.path,
- routes: routes,
- parentFullpath: '',
- parentSubloc: '',
- queryParams: uri.queryParameters,
- extra: extra,
- ).toList();
-
- assert(matchStacks.isNotEmpty, 'no routes for location: $location');
-
- // If there are multiple routes that match the location, returning the first one.
- // To make predefined routes to take precedence over dynamic routes eg. '/:id'
- // consider adding the dynamic route at the end of the routes
-
- return matchStacks.first;
- }
-
- /// turns a list of routes into a list of routes match stacks for the location
- /// e.g. routes: <GoRoute>[
- /// /
- /// family/:fid
- /// /login
- /// ]
- ///
- /// loc: /
- /// stacks: [
- /// matches: [
- /// match(route.path=/, loc=/)
- /// ]
- /// ]
- ///
- /// loc: /login
- /// stacks: [
- /// matches: [
- /// match(route.path=/login, loc=login)
- /// ]
- /// ]
- ///
- /// loc: /family/f2
- /// stacks: [
- /// matches: [
- /// match(route.path=/, loc=/),
- /// match(route.path=family/:fid, loc=family/f2, params=[fid=f2])
- /// ]
- /// ]
- ///
- /// loc: /family/f2/person/p1
- /// stacks: [
- /// matches: [
- /// match(route.path=/, loc=/),
- /// match(route.path=family/:fid, loc=family/f2, params=[fid=f2])
- /// match(route.path=person/:pid, loc=person/p1, params=[fid=f2, pid=p1])
- /// ]
- /// ]
- ///
- /// A stack count of 0 means there's no match.
- /// A stack count of >1 means there's a malformed set of routes.
- ///
- /// NOTE: Uses recursion, which is why _getLocRouteMatchStacks calls this
- /// function and does the actual error checking, using the returned stacks to
- /// provide better errors
- static Iterable<List<GoRouteMatch>> _getLocRouteMatchStacks({
- required String loc,
- required String restLoc,
- required String parentSubloc,
- required List<GoRoute> routes,
- required String parentFullpath,
- required Map<String, String> queryParams,
- required Object? extra,
- }) sync* {
- // find the set of matches at this level of the tree
- for (final GoRoute route in routes) {
- final String fullpath = fullLocFor(parentFullpath, route.path);
- final GoRouteMatch? match = GoRouteMatch.match(
- route: route,
- restLoc: restLoc,
- parentSubloc: parentSubloc,
- path: route.path,
- fullpath: fullpath,
- queryParams: queryParams,
- extra: extra,
- );
- if (match == null) {
- continue;
- }
-
- // if we have a complete match, then return the matched route
- // NOTE: need a lower case match because subloc is canonicalized to match
- // the path case whereas the location can be of any case and still match
- if (match.subloc.toLowerCase() == loc.toLowerCase()) {
- yield <GoRouteMatch>[match];
- continue;
- }
-
- // if we have a partial match but no sub-routes, bail
- if (route.routes.isEmpty) {
- continue;
- }
-
- // otherwise recurse
- final String childRestLoc =
- loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1));
- assert(loc.startsWith(match.subloc));
- assert(restLoc.isNotEmpty);
-
- // if there's no sub-route matches, then we don't have a match for this
- // location
- final List<List<GoRouteMatch>> subRouteMatchStacks =
- _getLocRouteMatchStacks(
- loc: loc,
- restLoc: childRestLoc,
- parentSubloc: match.subloc,
- routes: route.routes,
- parentFullpath: fullpath,
- queryParams: queryParams,
- extra: extra,
- ).toList();
- if (subRouteMatchStacks.isEmpty) {
- continue;
- }
-
- // add the match to each of the sub-route match stacks and return them
- for (final List<GoRouteMatch> stack in subRouteMatchStacks) {
- yield <GoRouteMatch>[match, ...stack];
- }
- }
- }
-
- GoRouteMatch? _getNameRouteMatch(
- String name, {
- Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
- Object? extra,
- }) {
- final GoRouteMatch? partialMatch = _namedMatches[name];
- return partialMatch == null
- ? null
- : GoRouteMatch.matchNamed(
- name: name,
- route: partialMatch.route,
- fullpath: partialMatch.fullpath,
- params: params,
- queryParams: queryParams,
- extra: extra,
- );
- }
-
- // e.g.
- // parentFullLoc: '', path => '/'
- // parentFullLoc: '/', path => 'family/:fid' => '/family/:fid'
- // parentFullLoc: '/', path => 'family/f2' => '/family/f2'
- // parentFullLoc: '/family/f2', path => 'parent/p1' => '/family/f2/person/p1'
- // ignore: public_member_api_docs
- static String fullLocFor(String parentFullLoc, String path) {
- // at the root, just return the path
- if (parentFullLoc.isEmpty) {
- assert(path.startsWith('/'));
- assert(path == '/' || !path.endsWith('/'));
- return path;
- }
-
- // not at the root, so append the parent path
- assert(path.isNotEmpty);
- assert(!path.startsWith('/'));
- assert(!path.endsWith('/'));
- return '${parentFullLoc == '/' ? '' : parentFullLoc}/$path';
- }
-
Widget _builder(BuildContext context, Iterable<GoRouteMatch> matches) {
List<Page<dynamic>>? pages;
Exception? error;
-
+ final String location = matches.last.fullUriString;
try {
// build the stack of pages
if (routerNeglect) {
@@ -620,7 +118,10 @@
// `redirect` impl
// ignore: avoid_catches_without_on_clauses
} catch (err, stack) {
- log.severe('Exception during GoRouter navigation', err, stack);
+ assert(() {
+ log.severe('Exception during GoRouter navigation', err, stack);
+ return true;
+ }());
// if there's an error, show an error page
error = err is Exception ? err : Exception(err);
@@ -629,7 +130,7 @@
_errorPageBuilder(
context,
GoRouterState(
- this,
+ _parser,
location: location,
subloc: uri.path,
name: null,
@@ -654,7 +155,7 @@
return builderWithNav(
context,
GoRouterState(
- this,
+ _parser,
location: location,
name: null, // no name available at the top level
// trim the query params off the subloc to match route.redirect
@@ -715,20 +216,24 @@
// get a page from the builder and associate it with a sub-location
final GoRouterState state = GoRouterState(
- this,
- location: location,
+ _parser,
+ location: match.fullUriString,
subloc: match.subloc,
name: match.route.name,
path: match.route.path,
fullpath: match.fullpath,
params: params,
+ error: match.error,
queryParams: match.queryParams,
extra: match.extra,
pageKey: match.pageKey, // push() remaps the page key for uniqueness
);
+ if (match.error != null) {
+ yield _errorPageBuilder(context, state);
+ break;
+ }
final GoRouterPageBuilder? pageBuilder = match.route.pageBuilder;
-
Page<dynamic>? page;
if (pageBuilder != null) {
page = pageBuilder(context, state);
@@ -763,17 +268,26 @@
final Element? elem = context is Element ? context : null;
if (elem != null && isMaterialApp(elem)) {
- log.info('MaterialApp found');
+ assert(() {
+ log.info('MaterialApp found');
+ return true;
+ }());
_pageBuilderForAppType = pageBuilderForMaterialApp;
_errorBuilderForAppType = (BuildContext c, GoRouterState s) =>
GoRouterMaterialErrorScreen(s.error);
} else if (elem != null && isCupertinoApp(elem)) {
- log.info('CupertinoApp found');
+ assert(() {
+ log.info('CupertinoApp found');
+ return true;
+ }());
_pageBuilderForAppType = pageBuilderForCupertinoApp;
_errorBuilderForAppType = (BuildContext c, GoRouterState s) =>
GoRouterCupertinoErrorScreen(s.error);
} else {
- log.info('WidgetsApp assumed');
+ assert(() {
+ log.info('WidgetsApp found');
+ return true;
+ }());
_pageBuilderForAppType = pageBuilderForWidgetApp;
_errorBuilderForAppType =
(BuildContext c, GoRouterState s) => GoRouterErrorScreen(s.error);
@@ -835,54 +349,4 @@
errorBuilder ?? _errorBuilderForAppType!,
);
}
-
- void _outputKnownRoutes() {
- log.info('known full paths for routes:');
- _outputFullPathsFor(routes, '', 0);
-
- if (_namedMatches.isNotEmpty) {
- log.info('known full paths for route names:');
- for (final MapEntry<String, GoRouteMatch> e in _namedMatches.entries) {
- log.info(' ${e.key} => ${e.value.fullpath}');
- }
- }
- }
-
- void _outputFullPathsFor(
- List<GoRoute> routes,
- String parentFullpath,
- int depth,
- ) {
- for (final GoRoute route in routes) {
- final String fullpath = fullLocFor(parentFullpath, route.path);
- log.info(' => ${''.padLeft(depth * 2)}$fullpath');
- _outputFullPathsFor(route.routes, fullpath, depth + 1);
- }
- }
-
- static String _canonicalUri(String loc) {
- String canon = Uri.parse(loc).toString();
- canon = canon.endsWith('?') ? canon.substring(0, canon.length - 1) : canon;
-
- // remove trailing slash except for when you shouldn't, e.g.
- // /profile/ => /profile
- // / => /
- // /login?from=/ => login?from=/
- canon = canon.endsWith('/') && canon != '/' && !canon.contains('?')
- ? canon.substring(0, canon.length - 1)
- : canon;
-
- // /login/?from=/ => /login?from=/
- // /?from=/ => /?from=/
- canon = canon.replaceFirst('/?', '?', 1);
-
- return canon;
- }
-
- static String _addQueryParams(String loc, Map<String, String> queryParams) {
- final Uri uri = Uri.parse(loc);
- assert(uri.queryParameters.isEmpty);
- return _canonicalUri(
- Uri(path: uri.path, queryParameters: queryParams).toString());
- }
}
diff --git a/packages/go_router/lib/src/go_router_state.dart b/packages/go_router/lib/src/go_router_state.dart
index ffcb072..9db7834 100644
--- a/packages/go_router/lib/src/go_router_state.dart
+++ b/packages/go_router/lib/src/go_router_state.dart
@@ -4,7 +4,7 @@
import 'package:flutter/foundation.dart';
-import 'go_router_delegate.dart';
+import 'go_route_information_parser.dart';
/// The route state during routing.
class GoRouterState {
@@ -29,7 +29,8 @@
: subloc),
assert((path ?? '').isEmpty == (fullpath ?? '').isEmpty);
- final GoRouterDelegate _delegate;
+ // TODO(chunhtai): remove this once namedLocation is removed from go_router.
+ final GoRouteInformationParser _delegate;
/// The full location of the route, e.g. /family/f2/person/p1
final String location;
@@ -67,6 +68,8 @@
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
- }) =>
- _delegate.namedLocation(name, params: params, queryParams: queryParams);
+ }) {
+ return _delegate.namedLocation(name,
+ params: params, queryParams: queryParams);
+ }
}
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index 552c431..0743dad 100644
--- a/packages/go_router/pubspec.yaml
+++ b/packages/go_router/pubspec.yaml
@@ -1,13 +1,12 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
-version: 3.1.1
+version: 4.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
environment:
sdk: ">=2.12.0 <3.0.0"
- flutter: ">=2.0.0"
dependencies:
collection: ^1.15.0
diff --git a/packages/go_router/test/custom_transition_page_test.dart b/packages/go_router/test/custom_transition_page_test.dart
index 98ac8cd..28c622a 100644
--- a/packages/go_router/test/custom_transition_page_test.dart
+++ b/packages/go_router/test/custom_transition_page_test.dart
@@ -24,6 +24,7 @@
);
await tester.pumpWidget(
MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
diff --git a/packages/go_router/test/error_screen_helpers.dart b/packages/go_router/test/error_screen_helpers.dart
index c8ae740..c33d328 100644
--- a/packages/go_router/test/error_screen_helpers.dart
+++ b/packages/go_router/test/error_screen_helpers.dart
@@ -51,6 +51,7 @@
Widget materialAppRouterBuilder(GoRouter router) {
return MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
@@ -59,6 +60,7 @@
Widget cupertinoAppRouterBuilder(GoRouter router) {
return CupertinoApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
diff --git a/packages/go_router/test/go_route_information_parser_test.dart b/packages/go_router/test/go_route_information_parser_test.dart
new file mode 100644
index 0000000..426d584
--- /dev/null
+++ b/packages/go_router/test/go_route_information_parser_test.dart
@@ -0,0 +1,192 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:go_router/go_router.dart';
+import 'package:go_router/src/go_route_information_parser.dart';
+import 'package:go_router/src/go_route_match.dart';
+
+void main() {
+ test('GoRouteInformationParser can parse route', () async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Placeholder(),
+ routes: <GoRoute>[
+ GoRoute(
+ path: 'abc',
+ builder: (_, __) => const Placeholder(),
+ ),
+ ],
+ ),
+ ];
+ final GoRouteInformationParser parser = GoRouteInformationParser(
+ routes: routes,
+ redirectLimit: 100,
+ topRedirect: (_) => null,
+ );
+
+ List<GoRouteMatch> matches = await parser
+ .parseRouteInformation(const RouteInformation(location: '/'));
+ expect(matches.length, 1);
+ expect(matches[0].queryParams.isEmpty, isTrue);
+ expect(matches[0].extra, isNull);
+ expect(matches[0].fullUriString, '/');
+ expect(matches[0].subloc, '/');
+ expect(matches[0].route, routes[0]);
+
+ final Object extra = Object();
+ matches = await parser.parseRouteInformation(
+ RouteInformation(location: '/abc?def=ghi', state: extra));
+ expect(matches.length, 2);
+ expect(matches[0].queryParams.length, 1);
+ expect(matches[0].queryParams['def'], 'ghi');
+ expect(matches[0].extra, extra);
+ expect(matches[0].fullUriString, '/?def=ghi');
+ expect(matches[0].subloc, '/');
+ expect(matches[0].route, routes[0]);
+
+ expect(matches[1].queryParams.length, 1);
+ expect(matches[1].queryParams['def'], 'ghi');
+ expect(matches[1].extra, extra);
+ expect(matches[1].fullUriString, '/abc?def=ghi');
+ expect(matches[1].subloc, '/abc');
+ expect(matches[1].route, routes[0].routes[0]);
+ });
+
+ test('GoRouteInformationParser returns error when unknown route', () async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Placeholder(),
+ routes: <GoRoute>[
+ GoRoute(
+ path: 'abc',
+ builder: (_, __) => const Placeholder(),
+ ),
+ ],
+ ),
+ ];
+ final GoRouteInformationParser parser = GoRouteInformationParser(
+ routes: routes,
+ redirectLimit: 100,
+ topRedirect: (_) => null,
+ );
+
+ final List<GoRouteMatch> matches = await parser
+ .parseRouteInformation(const RouteInformation(location: '/def'));
+ expect(matches.length, 1);
+ expect(matches[0].queryParams.isEmpty, isTrue);
+ expect(matches[0].extra, isNull);
+ expect(matches[0].fullUriString, '/def');
+ expect(matches[0].subloc, '/def');
+ expect(matches[0].error!.toString(),
+ 'Exception: no routes for location: /def');
+ });
+
+ test('GoRouteInformationParser can work with route parameters', () async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Placeholder(),
+ routes: <GoRoute>[
+ GoRoute(
+ path: ':uid/family/:fid',
+ builder: (_, __) => const Placeholder(),
+ ),
+ ],
+ ),
+ ];
+ final GoRouteInformationParser parser = GoRouteInformationParser(
+ routes: routes,
+ redirectLimit: 100,
+ topRedirect: (_) => null,
+ );
+
+ final List<GoRouteMatch> matches = await parser.parseRouteInformation(
+ const RouteInformation(location: '/123/family/456'));
+ expect(matches.length, 2);
+ expect(matches[0].queryParams.isEmpty, isTrue);
+ expect(matches[0].extra, isNull);
+ expect(matches[0].fullUriString, '/');
+ expect(matches[0].subloc, '/');
+
+ expect(matches[1].queryParams.isEmpty, isTrue);
+ expect(matches[1].extra, isNull);
+ expect(matches[1].fullUriString, '/123/family/456');
+ expect(matches[1].subloc, '/123/family/456');
+ expect(matches[1].encodedParams.length, 2);
+ expect(matches[1].encodedParams['uid'], '123');
+ expect(matches[1].encodedParams['fid'], '456');
+ });
+
+ test('GoRouteInformationParser can do top level redirect', () async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Placeholder(),
+ routes: <GoRoute>[
+ GoRoute(
+ path: ':uid/family/:fid',
+ builder: (_, __) => const Placeholder(),
+ ),
+ ],
+ ),
+ ];
+ final GoRouteInformationParser parser = GoRouteInformationParser(
+ routes: routes,
+ redirectLimit: 100,
+ topRedirect: (GoRouterState state) {
+ if (state.location != '/123/family/345') {
+ return '/123/family/345';
+ }
+ return null;
+ },
+ );
+
+ final List<GoRouteMatch> matches = await parser
+ .parseRouteInformation(const RouteInformation(location: '/random/uri'));
+ expect(matches.length, 2);
+ expect(matches[0].fullUriString, '/');
+ expect(matches[0].subloc, '/');
+
+ expect(matches[1].fullUriString, '/123/family/345');
+ expect(matches[1].subloc, '/123/family/345');
+ });
+
+ test('GoRouteInformationParser can do route level redirect', () async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Placeholder(),
+ routes: <GoRoute>[
+ GoRoute(
+ path: ':uid/family/:fid',
+ builder: (_, __) => const Placeholder(),
+ ),
+ GoRoute(
+ path: 'redirect',
+ redirect: (_) => '/123/family/345',
+ builder: (_, __) => throw UnimplementedError(),
+ ),
+ ],
+ ),
+ ];
+ final GoRouteInformationParser parser = GoRouteInformationParser(
+ routes: routes,
+ redirectLimit: 100,
+ topRedirect: (_) => null,
+ );
+
+ final List<GoRouteMatch> matches = await parser
+ .parseRouteInformation(const RouteInformation(location: '/redirect'));
+ expect(matches.length, 2);
+ expect(matches[0].fullUriString, '/');
+ expect(matches[0].subloc, '/');
+
+ expect(matches[1].fullUriString, '/123/family/345');
+ expect(matches[1].subloc, '/123/family/345');
+ });
+}
diff --git a/packages/go_router/test/go_router_delegate_test.dart b/packages/go_router/test/go_router_delegate_test.dart
index a715332..9ad9745 100644
--- a/packages/go_router/test/go_router_delegate_test.dart
+++ b/packages/go_router/test/go_router_delegate_test.dart
@@ -6,12 +6,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/go_route_match.dart';
-import 'package:go_router/src/go_router_delegate.dart';
import 'package:go_router/src/go_router_error_page.dart';
-GoRouterDelegate createGoRouterDelegate({
+Future<GoRouter> createGoRouter(
+ WidgetTester tester, {
Listenable? refreshListenable,
-}) {
+}) async {
final GoRouter router = GoRouter(
initialLocation: '/',
routes: <GoRoute>[
@@ -23,26 +23,32 @@
],
refreshListenable: refreshListenable,
);
- return router.routerDelegate;
+ await tester.pumpWidget(MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
+ routeInformationParser: router.routeInformationParser,
+ routerDelegate: router.routerDelegate));
+ return router;
}
void main() {
group('pop', () {
- test('removes the last element', () {
- final GoRouterDelegate delegate = createGoRouterDelegate()
- ..push('/error')
- ..addListener(expectAsync0(() {}));
- final GoRouteMatch last = delegate.matches.last;
- delegate.pop();
- expect(delegate.matches.length, 1);
- expect(delegate.matches.contains(last), false);
+ testWidgets('removes the last element', (WidgetTester tester) async {
+ final GoRouter goRouter = await createGoRouter(tester)
+ ..push('/error');
+
+ goRouter.routerDelegate.addListener(expectAsync0(() {}));
+ final GoRouteMatch last = goRouter.routerDelegate.matches.last;
+ goRouter.routerDelegate.pop();
+ expect(goRouter.routerDelegate.matches.length, 1);
+ expect(goRouter.routerDelegate.matches.contains(last), false);
});
- test('throws when it pops more than matches count', () {
- final GoRouterDelegate delegate = createGoRouterDelegate()
+ testWidgets('throws when it pops more than matches count',
+ (WidgetTester tester) async {
+ final GoRouter goRouter = await createGoRouter(tester)
..push('/error');
expect(
- () => delegate
+ () => goRouter.routerDelegate
..pop()
..pop(),
throwsA(isAssertionError),
@@ -50,9 +56,13 @@
});
});
- test('dispose unsubscribes from refreshListenable', () {
+ testWidgets('dispose unsubscribes from refreshListenable',
+ (WidgetTester tester) async {
final FakeRefreshListenable refreshListenable = FakeRefreshListenable();
- createGoRouterDelegate(refreshListenable: refreshListenable).dispose();
+ final GoRouter goRouter =
+ await createGoRouter(tester, refreshListenable: refreshListenable);
+ await tester.pumpWidget(Container());
+ goRouter.dispose();
expect(refreshListenable.unsubscribed, true);
});
}
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index 8316597..ca6461b 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -24,7 +24,7 @@
Logger.root.onRecord.listen((LogRecord e) => debugPrint('$e'));
group('path routes', () {
- test('match home route', () {
+ testWidgets('match home route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -32,20 +32,21 @@
const HomeScreen()),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.fullpath, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
});
- test('If there is more than one route to match, use the first match', () {
+ testWidgets('If there is more than one route to match, use the first match',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/', builder: _dummy),
GoRoute(path: '/', builder: _dummy),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
@@ -89,28 +90,30 @@
}, throwsA(isAssertionError));
});
- test('lack of leading / on top-level route', () {
- expect(() {
+ testWidgets('lack of leading / on top-level route',
+ (WidgetTester tester) async {
+ await expectLater(() async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: 'foo', builder: _dummy),
];
- _router(routes);
+ await _router(routes, tester);
}, throwsA(isAssertionError));
});
- test('match no routes', () {
+ testWidgets('match no routes', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/', builder: _dummy),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/foo');
+ await tester.pumpAndSettle();
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
});
- test('match 2nd top level route', () {
+ testWidgets('match 2nd top level route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -122,7 +125,7 @@
const LoginScreen()),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/login');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
@@ -130,7 +133,8 @@
expect(router.screenFor(matches.first).runtimeType, LoginScreen);
});
- test('match top level route when location has trailing /', () {
+ testWidgets('match top level route when location has trailing /',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -144,7 +148,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/login/');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
@@ -152,13 +156,14 @@
expect(router.screenFor(matches.first).runtimeType, LoginScreen);
});
- test('match top level route when location has trailing / (2)', () {
+ testWidgets('match top level route when location has trailing / (2)',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/profile', redirect: (_) => '/profile/foo'),
GoRoute(path: '/profile/:kind', builder: _dummy),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/profile/');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
@@ -166,13 +171,14 @@
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
});
- test('match top level route when location has trailing / (3)', () {
+ testWidgets('match top level route when location has trailing / (3)',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/profile', redirect: (_) => '/profile/foo'),
GoRoute(path: '/profile/:kind', builder: _dummy),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/profile/?bar=baz');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
@@ -180,7 +186,7 @@
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
});
- test('match sub-route', () {
+ testWidgets('match sub-route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -196,7 +202,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/login');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches.length, 2);
@@ -206,7 +212,7 @@
expect(router.screenFor(matches[1]).runtimeType, LoginScreen);
});
- test('match sub-routes', () {
+ testWidgets('match sub-routes', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -234,7 +240,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
{
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
@@ -275,7 +281,8 @@
}
});
- test('return first matching route if too many subroutes', () {
+ testWidgets('return first matching route if too many subroutes',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -308,7 +315,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/bar');
List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(2));
@@ -325,28 +332,12 @@
expect(router.screenFor(matches[1]).runtimeType, Page2Screen);
});
- test('router state', () {
+ testWidgets('router state', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
path: '/',
builder: (BuildContext context, GoRouterState state) {
- expect(
- state.location,
- anyOf(<String>[
- '/',
- '/login',
- '/family/f2',
- '/family/f2/person/p1'
- ]),
- );
- expect(state.subloc, '/');
- expect(state.name, 'home');
- expect(state.path, '/');
- expect(state.fullpath, '/');
- expect(state.params, <String, String>{});
- expect(state.error, null);
- expect(state.extra! as int, 1);
return const HomeScreen();
},
routes: <GoRoute>[
@@ -408,14 +399,18 @@
),
];
- final GoRouter router = _router(routes);
- router.go('/', extra: 1);
- router.go('/login', extra: 2);
- router.go('/family/f2', extra: 3);
- router.go('/family/f2/person/p1', extra: 4);
+ final GoRouter router = await _router(routes, tester);
+
+ await tester.pump();
+ router.push('/login', extra: 2);
+ await tester.pump();
+ router.push('/family/f2', extra: 3);
+ await tester.pump();
+ router.push('/family/f2/person/p1', extra: 4);
+ await tester.pump();
});
- test('match path case insensitively', () {
+ testWidgets('match path case insensitively', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -429,7 +424,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
const String loc = '/FaMiLy/f2';
router.go(loc);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
@@ -443,7 +438,9 @@
expect(router.screenFor(matches.first).runtimeType, FamilyScreen);
});
- test('If there is more than one route to match, use the first match.', () {
+ testWidgets(
+ 'If there is more than one route to match, use the first match.',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(path: '/', builder: _dummy),
GoRoute(path: '/page1', builder: _dummy),
@@ -451,7 +448,7 @@
GoRoute(path: '/:ok', builder: _dummy),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/user');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
@@ -460,7 +457,7 @@
});
group('named routes', () {
- test('match home route', () {
+ testWidgets('match home route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
@@ -469,18 +466,18 @@
const HomeScreen()),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.goNamed('home');
});
- test('match too many routes', () {
+ testWidgets('match too many routes', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(name: 'home', path: '/', builder: _dummy),
GoRoute(name: 'home', path: '/', builder: _dummy),
];
- expect(() {
- _router(routes);
+ await expectLater(() async {
+ await _router(routes, tester);
}, throwsA(isAssertionError));
});
@@ -490,17 +487,17 @@
}, throwsA(isAssertionError));
});
- test('match no routes', () {
- expect(() {
+ testWidgets('match no routes', (WidgetTester tester) async {
+ await expectLater(() async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(name: 'home', path: '/', builder: _dummy),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.goNamed('work');
}, throwsA(isAssertionError));
});
- test('match 2nd top level route', () {
+ testWidgets('match 2nd top level route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
@@ -516,11 +513,11 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.goNamed('login');
});
- test('match sub-route', () {
+ testWidgets('match sub-route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
@@ -538,40 +535,11 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.goNamed('login');
});
- test('match sub-route case insensitive', () {
- final List<GoRoute> routes = <GoRoute>[
- GoRoute(
- name: 'home',
- path: '/',
- builder: (BuildContext context, GoRouterState state) =>
- const HomeScreen(),
- routes: <GoRoute>[
- GoRoute(
- name: 'page1',
- path: 'page1',
- builder: (BuildContext context, GoRouterState state) =>
- const Page1Screen(),
- ),
- GoRoute(
- name: 'page2',
- path: 'Page2',
- builder: (BuildContext context, GoRouterState state) =>
- const Page2Screen(),
- ),
- ],
- ),
- ];
-
- final GoRouter router = _router(routes);
- router.goNamed('Page1');
- router.goNamed('page2');
- });
-
- test('match w/ params', () {
+ testWidgets('match w/ params', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
@@ -600,12 +568,12 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
});
- test('too few params', () {
+ testWidgets('too few params', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
@@ -630,13 +598,15 @@
],
),
];
- expect(() {
- final GoRouter router = _router(routes);
+ await expectLater(() async {
+ final GoRouter router = await _router(routes, tester);
router.goNamed('person', params: <String, String>{'fid': 'f2'});
+ await tester.pump();
}, throwsA(isAssertionError));
});
- test('match case insensitive w/ params', () {
+ testWidgets('match case insensitive w/ params',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
@@ -665,12 +635,12 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
});
- test('too few params', () {
+ testWidgets('too few params', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'family',
@@ -679,13 +649,13 @@
const FamilyScreen('dummy'),
),
];
- expect(() {
- final GoRouter router = _router(routes);
+ await expectLater(() async {
+ final GoRouter router = await _router(routes, tester);
router.goNamed('family');
}, throwsA(isAssertionError));
});
- test('too many params', () {
+ testWidgets('too many params', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'family',
@@ -694,14 +664,14 @@
const FamilyScreen('dummy'),
),
];
- expect(() {
- final GoRouter router = _router(routes);
+ await expectLater(() async {
+ final GoRouter router = await _router(routes, tester);
router.goNamed('family',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
}, throwsA(isAssertionError));
});
- test('sparsely named routes', () {
+ testWidgets('sparsely named routes', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -726,7 +696,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
@@ -734,7 +704,8 @@
expect(router.screenFor(matches.last).runtimeType, PersonScreen);
});
- test('preserve path param spaces and slashes', () {
+ testWidgets('preserve path param spaces and slashes',
+ (WidgetTester tester) async {
const String param1 = 'param w/ spaces and slashes';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
@@ -747,7 +718,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
final String loc = router
.namedLocation('page1', params: <String, String>{'param1': param1});
log.info('loc= $loc');
@@ -759,7 +730,8 @@
expect(matches.first.decodedParams['param1'], param1);
});
- test('preserve query param spaces and slashes', () {
+ testWidgets('preserve query param spaces and slashes',
+ (WidgetTester tester) async {
const String param1 = 'param w/ spaces and slashes';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
@@ -772,21 +744,19 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
final String loc = router.namedLocation('page1',
queryParams: <String, String>{'param1': param1});
- log.info('loc= $loc');
router.go(loc);
-
+ await tester.pump();
final List<GoRouteMatch> matches = router.routerDelegate.matches;
- log.info('param1= ${matches.first.queryParams['param1']}');
expect(router.screenFor(matches.first).runtimeType, DummyScreen);
expect(matches.first.queryParams['param1'], param1);
});
});
group('redirects', () {
- test('top-level redirect', () {
+ testWidgets('top-level redirect', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -805,16 +775,15 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
- redirect: (GoRouterState state) =>
- state.subloc == '/login' ? null : '/login',
- );
+ final GoRouter router = await _router(routes, tester,
+ redirect: (GoRouterState state) =>
+ state.subloc == '/login' ? null : '/login');
+
expect(router.location, '/login');
});
- test('top-level redirect w/ named routes', () {
+ testWidgets('top-level redirect w/ named routes',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
@@ -838,17 +807,16 @@
),
];
- final GoRouter router = GoRouter(
- debugLogDiagnostics: true,
- routes: routes,
- errorBuilder: _dummy,
+ final GoRouter router = await _router(
+ routes,
+ tester,
redirect: (GoRouterState state) =>
state.subloc == '/login' ? null : state.namedLocation('login'),
);
expect(router.location, '/login');
});
- test('route-level redirect', () {
+ testWidgets('route-level redirect', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -870,15 +838,14 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
- );
+ final GoRouter router = await _router(routes, tester);
router.go('/dummy');
+ await tester.pump();
expect(router.location, '/login');
});
- test('route-level redirect w/ named routes', () {
+ testWidgets('route-level redirect w/ named routes',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
name: 'home',
@@ -903,15 +870,13 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
- );
+ final GoRouter router = await _router(routes, tester);
router.go('/dummy');
+ await tester.pump();
expect(router.location, '/login');
});
- test('multiple mixed redirect', () {
+ testWidgets('multiple mixed redirect', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -933,27 +898,21 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
- redirect: (GoRouterState state) =>
- state.subloc == '/dummy1' ? '/dummy2' : null,
- );
+ final GoRouter router = await _router(routes, tester,
+ redirect: (GoRouterState state) =>
+ state.subloc == '/dummy1' ? '/dummy2' : null);
router.go('/dummy1');
+ await tester.pump();
expect(router.location, '/');
});
- test('top-level redirect loop', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[],
- errorBuilder: (BuildContext context, GoRouterState state) =>
- ErrorScreen(state.error!),
- redirect: (GoRouterState state) => state.subloc == '/'
- ? '/login'
- : state.subloc == '/login'
- ? '/'
- : null,
- );
+ testWidgets('top-level redirect loop', (WidgetTester tester) async {
+ final GoRouter router = await _router(<GoRoute>[], tester,
+ redirect: (GoRouterState state) => state.subloc == '/'
+ ? '/login'
+ : state.subloc == '/login'
+ ? '/'
+ : null);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
@@ -962,9 +921,9 @@
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
- test('route-level redirect loop', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[
+ testWidgets('route-level redirect loop', (WidgetTester tester) async {
+ final GoRouter router = await _router(
+ <GoRoute>[
GoRoute(
path: '/',
redirect: (GoRouterState state) => '/login',
@@ -974,8 +933,7 @@
redirect: (GoRouterState state) => '/',
),
],
- errorBuilder: (BuildContext context, GoRouterState state) =>
- ErrorScreen(state.error!),
+ tester,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
@@ -985,16 +943,15 @@
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
- test('mixed redirect loop', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[
+ testWidgets('mixed redirect loop', (WidgetTester tester) async {
+ final GoRouter router = await _router(
+ <GoRoute>[
GoRoute(
path: '/login',
redirect: (GoRouterState state) => '/',
),
],
- errorBuilder: (BuildContext context, GoRouterState state) =>
- ErrorScreen(state.error!),
+ tester,
redirect: (GoRouterState state) =>
state.subloc == '/' ? '/login' : null,
);
@@ -1006,11 +963,11 @@
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
- test('top-level redirect loop w/ query params', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[],
- errorBuilder: (BuildContext context, GoRouterState state) =>
- ErrorScreen(state.error!),
+ testWidgets('top-level redirect loop w/ query params',
+ (WidgetTester tester) async {
+ final GoRouter router = await _router(
+ <GoRoute>[],
+ tester,
redirect: (GoRouterState state) => state.subloc == '/'
? '/login?from=${state.location}'
: state.subloc == '/login'
@@ -1025,7 +982,8 @@
log.info((router.screenFor(matches.first) as ErrorScreen).ex);
});
- test('expect null path/fullpath on top-level redirect', () {
+ testWidgets('expect null path/fullpath on top-level redirect',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -1038,15 +996,15 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
+ final GoRouter router = await _router(
+ routes,
+ tester,
initialLocation: '/dummy',
);
expect(router.location, '/');
});
- test('top-level redirect state', () {
+ testWidgets('top-level redirect state', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -1060,11 +1018,10 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
+ final GoRouter router = await _router(
+ routes,
+ tester,
initialLocation: '/login?from=/',
- debugLogDiagnostics: true,
redirect: (GoRouterState state) {
expect(Uri.parse(state.location).queryParameters, isNotEmpty);
expect(Uri.parse(state.subloc).queryParameters, isEmpty);
@@ -1082,7 +1039,7 @@
expect(router.screenFor(matches.first).runtimeType, LoginScreen);
});
- test('route-level redirect state', () {
+ testWidgets('route-level redirect state', (WidgetTester tester) async {
const String loc = '/book/0';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
@@ -1100,11 +1057,10 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
+ final GoRouter router = await _router(
+ routes,
+ tester,
initialLocation: loc,
- debugLogDiagnostics: true,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
@@ -1112,7 +1068,8 @@
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
});
- test('sub-sub-route-level redirect params', () {
+ testWidgets('sub-sub-route-level redirect params',
+ (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -1141,11 +1098,10 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
+ final GoRouter router = await _router(
+ routes,
+ tester,
initialLocation: '/family/f2/person/p1',
- debugLogDiagnostics: true,
);
final List<GoRouteMatch> matches = router.routerDelegate.matches;
@@ -1157,12 +1113,10 @@
expect(page.pid, 'p1');
});
- test('redirect limit', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[],
- errorBuilder: (BuildContext context, GoRouterState state) =>
- ErrorScreen(state.error!),
- debugLogDiagnostics: true,
+ testWidgets('redirect limit', (WidgetTester tester) async {
+ final GoRouter router = await _router(
+ <GoRoute>[],
+ tester,
redirect: (GoRouterState state) => '${state.location}+',
redirectLimit: 10,
);
@@ -1176,7 +1130,7 @@
});
group('initial location', () {
- test('initial location', () {
+ testWidgets('initial location', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -1192,15 +1146,15 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
+ final GoRouter router = await _router(
+ routes,
+ tester,
initialLocation: '/dummy',
);
expect(router.location, '/dummy');
});
- test('initial location w/ redirection', () {
+ testWidgets('initial location w/ redirection', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -1213,9 +1167,9 @@
),
];
- final GoRouter router = GoRouter(
- routes: routes,
- errorBuilder: _dummy,
+ final GoRouter router = await _router(
+ routes,
+ tester,
initialLocation: '/dummy',
);
expect(router.location, '/');
@@ -1223,7 +1177,7 @@
});
group('params', () {
- test('preserve path param case', () {
+ testWidgets('preserve path param case', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -1237,7 +1191,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
for (final String fid in <String>['f2', 'F2']) {
final String loc = '/family/$fid';
router.go(loc);
@@ -1250,7 +1204,7 @@
}
});
- test('preserve query param case', () {
+ testWidgets('preserve query param case', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -1265,7 +1219,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
for (final String fid in <String>['f2', 'F2']) {
final String loc = '/family?fid=$fid';
router.go(loc);
@@ -1278,7 +1232,8 @@
}
});
- test('preserve path param spaces and slashes', () {
+ testWidgets('preserve path param spaces and slashes',
+ (WidgetTester tester) async {
const String param1 = 'param w/ spaces and slashes';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
@@ -1290,7 +1245,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
final String loc = '/page1/${Uri.encodeComponent(param1)}';
router.go(loc);
@@ -1300,7 +1255,8 @@
expect(matches.first.decodedParams['param1'], param1);
});
- test('preserve query param spaces and slashes', () {
+ testWidgets('preserve query param spaces and slashes',
+ (WidgetTester tester) async {
const String param1 = 'param w/ spaces and slashes';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
@@ -1312,7 +1268,7 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
router.go('/page1?param1=$param1');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
@@ -1346,9 +1302,9 @@
}
});
- test('duplicate query param', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[
+ testWidgets('duplicate query param', (WidgetTester tester) async {
+ final GoRouter router = await _router(
+ <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
@@ -1360,20 +1316,18 @@
},
),
],
- errorBuilder: (BuildContext context, GoRouterState state) =>
- ErrorScreen(state.error!),
+ tester,
+ initialLocation: '/?id=0&id=1',
);
-
- router.go('/?id=0&id=1');
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.fullpath, '/');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
});
- test('duplicate path + query param', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[
+ testWidgets('duplicate path + query param', (WidgetTester tester) async {
+ final GoRouter router = await _router(
+ <GoRoute>[
GoRoute(
path: '/:id',
builder: (BuildContext context, GoRouterState state) {
@@ -1383,19 +1337,20 @@
},
),
],
- errorBuilder: _dummy,
+ tester,
);
router.go('/0?id=1');
+ await tester.pump();
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(matches, hasLength(1));
expect(matches.first.fullpath, '/:id');
expect(router.screenFor(matches.first).runtimeType, HomeScreen);
});
- test('push + query param', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[
+ testWidgets('push + query param', (WidgetTester tester) async {
+ final GoRouter router = await _router(
+ <GoRoute>[
GoRoute(path: '/', builder: _dummy),
GoRoute(
path: '/family',
@@ -1413,11 +1368,13 @@
),
),
],
- errorBuilder: _dummy,
+ tester,
);
router.go('/family?fid=f2');
+ await tester.pump();
router.push('/person?fid=f2&pid=p1');
+ await tester.pump();
final FamilyScreen page1 =
router.screenFor(router.routerDelegate.matches.first) as FamilyScreen;
expect(page1.fid, 'f2');
@@ -1428,9 +1385,9 @@
expect(page2.pid, 'p1');
});
- test('push + extra param', () {
- final GoRouter router = GoRouter(
- routes: <GoRoute>[
+ testWidgets('push + extra param', (WidgetTester tester) async {
+ final GoRouter router = await _router(
+ <GoRoute>[
GoRoute(path: '/', builder: _dummy),
GoRoute(
path: '/family',
@@ -1448,11 +1405,13 @@
),
),
],
- errorBuilder: _dummy,
+ tester,
);
router.go('/family', extra: <String, String>{'fid': 'f2'});
+ await tester.pump();
router.push('/person', extra: <String, String>{'fid': 'f2', 'pid': 'p1'});
+ await tester.pump();
final FamilyScreen page1 =
router.screenFor(router.routerDelegate.matches.first) as FamilyScreen;
expect(page1.fid, 'f2');
@@ -1463,7 +1422,7 @@
expect(page2.pid, 'p1');
});
- test('keep param in nested route', () {
+ testWidgets('keep param in nested route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
@@ -1488,12 +1447,13 @@
),
];
- final GoRouter router = _router(routes);
+ final GoRouter router = await _router(routes, tester);
const String fid = 'f1';
const String pid = 'p2';
const String loc = '/family/$fid/person/$pid';
router.push(loc);
+ await tester.pump();
final List<GoRouteMatch> matches = router.routerDelegate.matches;
expect(router.location, loc);
@@ -1586,6 +1546,7 @@
GoRouterNamedLocationSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
@@ -1605,6 +1566,7 @@
final GoRouterGoSpy router = GoRouterGoSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
@@ -1623,6 +1585,7 @@
final GoRouterGoNamedSpy router = GoRouterGoNamedSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
@@ -1645,6 +1608,7 @@
final GoRouterPushSpy router = GoRouterPushSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
@@ -1663,6 +1627,7 @@
final GoRouterPushNamedSpy router = GoRouterPushNamedSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
@@ -1681,9 +1646,11 @@
});
testWidgets('calls [pop] on closest GoRouter', (WidgetTester tester) async {
+ print('run 2.2');
final GoRouterPopSpy router = GoRouterPopSpy(routes: routes);
await tester.pumpWidget(
MaterialApp.router(
+ routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
title: 'GoRouter Example',
@@ -1694,20 +1661,17 @@
});
});
- test('pop triggers pop on routerDelegate', () {
- final GoRouter router = createGoRouter()..push('/error');
+ testWidgets('pop triggers pop on routerDelegate',
+ (WidgetTester tester) async {
+ final GoRouter router = await createGoRouter(tester)
+ ..push('/error');
router.routerDelegate.addListener(expectAsync0(() {}));
router.pop();
+ await tester.pump();
});
- test('refresh triggers refresh on routerDelegate', () {
- final GoRouter router = createGoRouter();
- router.routerDelegate.addListener(expectAsync0(() {}));
- router.refresh();
- });
-
- test('didPush notifies listeners', () {
- createGoRouter()
+ testWidgets('didPush notifies listeners', (WidgetTester tester) async {
+ await createGoRouter(tester)
..addListener(expectAsync0(() {}))
..didPush(
MaterialPageRoute<void>(builder: (_) => const Text('Current route')),
@@ -1715,8 +1679,8 @@
);
});
- test('didPop notifies listeners', () {
- createGoRouter()
+ testWidgets('didPop notifies listeners', (WidgetTester tester) async {
+ await createGoRouter(tester)
..addListener(expectAsync0(() {}))
..didPop(
MaterialPageRoute<void>(builder: (_) => const Text('Current route')),
@@ -1724,8 +1688,8 @@
);
});
- test('didRemove notifies listeners', () {
- createGoRouter()
+ testWidgets('didRemove notifies listeners', (WidgetTester tester) async {
+ await createGoRouter(tester)
..addListener(expectAsync0(() {}))
..didRemove(
MaterialPageRoute<void>(builder: (_) => const Text('Current route')),
@@ -1733,8 +1697,8 @@
);
});
- test('didReplace notifies listeners', () {
- createGoRouter()
+ testWidgets('didReplace notifies listeners', (WidgetTester tester) async {
+ await createGoRouter(tester)
..addListener(expectAsync0(() {}))
..didReplace(
newRoute: MaterialPageRoute<void>(
@@ -1746,23 +1710,11 @@
);
});
- test('uses navigatorBuilder when provided', () {
- final Func3<Widget, BuildContext, GoRouterState, Widget> navigationBuilder =
+ testWidgets('uses navigatorBuilder when provided',
+ (WidgetTester tester) async {
+ final Func3<Widget, BuildContext, GoRouterState, Widget> navigatorBuilder =
expectAsync3(fakeNavigationBuilder);
- final GoRouter router = createGoRouter(navigatorBuilder: navigationBuilder);
- final GoRouterDelegate delegate = router.routerDelegate;
- delegate.builderWithNav(
- DummyBuildContext(),
- GoRouterState(delegate, location: '/foo', subloc: '/bar', name: 'baz'),
- const Navigator(),
- );
- });
-}
-
-GoRouter createGoRouter({
- GoRouterNavigatorBuilder? navigatorBuilder,
-}) =>
- GoRouter(
+ final GoRouter router = GoRouter(
initialLocation: '/',
routes: <GoRoute>[
GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
@@ -1774,6 +1726,38 @@
navigatorBuilder: navigatorBuilder,
);
+ final GoRouterDelegate delegate = router.routerDelegate;
+ delegate.builderWithNav(
+ DummyBuildContext(),
+ GoRouterState(router.routeInformationParser,
+ location: '/foo', subloc: '/bar', name: 'baz'),
+ const Navigator(),
+ );
+ });
+}
+
+Future<GoRouter> createGoRouter(
+ WidgetTester tester, {
+ GoRouterNavigatorBuilder? navigatorBuilder,
+}) async {
+ final GoRouter goRouter = GoRouter(
+ initialLocation: '/',
+ routes: <GoRoute>[
+ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
+ GoRoute(
+ path: '/error',
+ builder: (_, __) => const GoRouterErrorScreen(null),
+ ),
+ ],
+ navigatorBuilder: navigatorBuilder,
+ );
+ await tester.pumpWidget(MaterialApp.router(
+ routeInformationProvider: goRouter.routeInformationProvider,
+ routeInformationParser: goRouter.routeInformationParser,
+ routerDelegate: goRouter.routerDelegate));
+ return goRouter;
+}
+
Widget fakeNavigationBuilder(
BuildContext context,
GoRouterState state,
@@ -1898,12 +1882,31 @@
}
}
-GoRouter _router(List<GoRoute> routes) => GoRouter(
- routes: routes,
- errorBuilder: (BuildContext context, GoRouterState state) =>
- ErrorScreen(state.error!),
- debugLogDiagnostics: true,
- );
+Future<GoRouter> _router(
+ List<GoRoute> routes,
+ WidgetTester tester, {
+ GoRouterRedirect? redirect,
+ String initialLocation = '/',
+ int redirectLimit = 5,
+}) async {
+ final GoRouter goRouter = GoRouter(
+ routes: routes,
+ redirect: redirect,
+ initialLocation: initialLocation,
+ redirectLimit: redirectLimit,
+ errorBuilder: (BuildContext context, GoRouterState state) =>
+ ErrorScreen(state.error!),
+ debugLogDiagnostics: true,
+ );
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routeInformationProvider: goRouter.routeInformationProvider,
+ routeInformationParser: goRouter.routeInformationParser,
+ routerDelegate: goRouter.routerDelegate,
+ ),
+ );
+ return goRouter;
+}
class ErrorScreen extends DummyScreen {
const ErrorScreen(this.ex, {Key? key}) : super(key: key);
@@ -1946,7 +1949,7 @@
const DummyScreen({Key? key}) : super(key: key);
@override
- Widget build(BuildContext context) => throw UnimplementedError();
+ Widget build(BuildContext context) => const Placeholder();
}
Widget _dummy(BuildContext context, GoRouterState state) => const DummyScreen();
@@ -1961,7 +1964,7 @@
}
Widget screenFor(GoRouteMatch match) =>
- (_pageFor(match) as NoTransitionPage<void>).child;
+ (_pageFor(match) as MaterialPage<void>).child;
}
class DummyBuildContext implements BuildContext {