From d39ffb1c93d2362ec95225422193c813f244a8bb Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 1 Jun 2022 15:29:56 -0400 Subject: [PATCH] Revert "[go_router] Refactor RouterDelegate into functional pieces (#1653)" (#2183) This reverts commit 2c02052ce8ba83edee76fef41a2475edab824c17. --- packages/go_router/CHANGELOG.md | 6 +- packages/go_router/README.md | 1 - .../go_router/example/lib/async_data.dart | 1 - .../go_router/example/lib/books/main.dart | 1 - packages/go_router/example/lib/cupertino.dart | 1 - .../go_router/example/lib/error_screen.dart | 1 - .../go_router/example/lib/extra_param.dart | 1 - packages/go_router/example/lib/init_loc.dart | 1 - .../go_router/example/lib/loading_page.dart | 1 - packages/go_router/example/lib/main.dart | 1 - .../go_router/example/lib/named_routes.dart | 1 - .../go_router/example/lib/nav_builder.dart | 1 - .../go_router/example/lib/nav_observer.dart | 1 - .../go_router/example/lib/nested_nav.dart | 1 - packages/go_router/example/lib/push.dart | 1 - .../go_router/example/lib/query_params.dart | 1 - .../go_router/example/lib/redirection.dart | 1 - .../go_router/example/lib/router_neglect.dart | 1 - .../example/lib/router_stream_refresh.dart | 1 - .../example/lib/shared_scaffold.dart | 1 - .../example/lib/state_restoration.dart | 1 - .../go_router/example/lib/sub_routes.dart | 1 - .../go_router/example/lib/transitions.dart | 1 - .../go_router/example/lib/url_strategy.dart | 1 - .../go_router/example/lib/user_input.dart | 1 - .../go_router/example/lib/widgets_app.dart | 1 - .../lib/src/go_route_information_parser.dart | 431 +----------- .../src/go_route_information_provider.dart | 120 ---- .../go_router/lib/src/go_route_match.dart | 73 ++- packages/go_router/lib/src/go_router.dart | 79 +-- .../go_router/lib/src/go_router_delegate.dart | 616 ++++++++++++++++-- .../go_router/lib/src/go_router_state.dart | 11 +- packages/go_router/pubspec.yaml | 3 +- .../test/custom_transition_page_test.dart | 1 - .../go_router/test/error_screen_helpers.dart | 2 - .../go_route_information_parser_test.dart | 192 ------ .../test/go_router_delegate_test.dart | 44 +- packages/go_router/test/go_router_test.dart | 575 ++++++++-------- 38 files changed, 964 insertions(+), 1214 deletions(-) delete mode 100644 packages/go_router/lib/src/go_route_information_provider.dart delete mode 100644 packages/go_router/test/go_route_information_parser_test.dart diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index ea232e2066..a37fabe336 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,14 +1,10 @@ -## 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 -- Adds `GoRouteData` and `TypedGoRoute` to support `package:go_router_builder`. +- Added `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 83dbc209b7..85f0712bb8 100644 --- a/packages/go_router/README.md +++ b/packages/go_router/README.md @@ -15,7 +15,6 @@ class App extends StatelessWidget { @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 5dfa0037fb..2a87a83cdc 100644 --- a/packages/go_router/example/lib/async_data.dart +++ b/packages/go_router/example/lib/async_data.dart @@ -26,7 +26,6 @@ class App extends StatelessWidget { @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 241c36861a..7f2059408d 100644 --- a/packages/go_router/example/lib/books/main.dart +++ b/packages/go_router/example/lib/books/main.dart @@ -31,7 +31,6 @@ class Bookstore extends StatelessWidget { 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 3f4bd10184..35112957d9 100644 --- a/packages/go_router/example/lib/cupertino.dart +++ b/packages/go_router/example/lib/cupertino.dart @@ -17,7 +17,6 @@ class App extends StatelessWidget { @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 a578a832e9..984441bdf8 100644 --- a/packages/go_router/example/lib/error_screen.dart +++ b/packages/go_router/example/lib/error_screen.dart @@ -17,7 +17,6 @@ class App extends StatelessWidget { @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 efab7bec9e..5e9141aff2 100644 --- a/packages/go_router/example/lib/extra_param.dart +++ b/packages/go_router/example/lib/extra_param.dart @@ -27,7 +27,6 @@ class App extends StatelessWidget { 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 899c3c13cf..4bef656a90 100644 --- a/packages/go_router/example/lib/init_loc.dart +++ b/packages/go_router/example/lib/init_loc.dart @@ -17,7 +17,6 @@ class App extends StatelessWidget { @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 9f61726087..2afb21e96c 100644 --- a/packages/go_router/example/lib/loading_page.dart +++ b/packages/go_router/example/lib/loading_page.dart @@ -55,7 +55,6 @@ class App extends StatelessWidget { Widget build(BuildContext context) => ChangeNotifierProvider.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 91b827b501..ef33494a57 100644 --- a/packages/go_router/example/lib/main.dart +++ b/packages/go_router/example/lib/main.dart @@ -17,7 +17,6 @@ class App extends StatelessWidget { @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 859a4bc5d9..648610d2ec 100644 --- a/packages/go_router/example/lib/named_routes.dart +++ b/packages/go_router/example/lib/named_routes.dart @@ -24,7 +24,6 @@ class App extends StatelessWidget { Widget build(BuildContext context) => ChangeNotifierProvider.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 8ec6b07e7d..ba8818f4d7 100644 --- a/packages/go_router/example/lib/nav_builder.dart +++ b/packages/go_router/example/lib/nav_builder.dart @@ -22,7 +22,6 @@ class App extends StatelessWidget { @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 5afd016a45..51cf45abb7 100644 --- a/packages/go_router/example/lib/nav_observer.dart +++ b/packages/go_router/example/lib/nav_observer.dart @@ -18,7 +18,6 @@ class App extends StatelessWidget { @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 04b8285ec6..5940f73c19 100644 --- a/packages/go_router/example/lib/nested_nav.dart +++ b/packages/go_router/example/lib/nested_nav.dart @@ -19,7 +19,6 @@ class App extends StatelessWidget { @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 1288f12f6d..9d24495494 100644 --- a/packages/go_router/example/lib/push.dart +++ b/packages/go_router/example/lib/push.dart @@ -17,7 +17,6 @@ class App extends StatelessWidget { @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 656273e24c..9a660ae4d1 100644 --- a/packages/go_router/example/lib/query_params.dart +++ b/packages/go_router/example/lib/query_params.dart @@ -25,7 +25,6 @@ class App extends StatelessWidget { Widget build(BuildContext context) => ChangeNotifierProvider.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 0cbe6ed127..395e72719c 100644 --- a/packages/go_router/example/lib/redirection.dart +++ b/packages/go_router/example/lib/redirection.dart @@ -25,7 +25,6 @@ class App extends StatelessWidget { Widget build(BuildContext context) => ChangeNotifierProvider.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 4f4acfb825..0e3ebaccee 100644 --- a/packages/go_router/example/lib/router_neglect.dart +++ b/packages/go_router/example/lib/router_neglect.dart @@ -17,7 +17,6 @@ class App extends StatelessWidget { @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 2d00dbe4ef..cb4607edd3 100644 --- a/packages/go_router/example/lib/router_stream_refresh.dart +++ b/packages/go_router/example/lib/router_stream_refresh.dart @@ -94,7 +94,6 @@ class _AppState extends State { Widget build(BuildContext context) => Provider.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 3dc0c109b3..8699f67233 100644 --- a/packages/go_router/example/lib/shared_scaffold.dart +++ b/packages/go_router/example/lib/shared_scaffold.dart @@ -19,7 +19,6 @@ class App extends StatelessWidget { @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 33d64cac58..695fa86a96 100644 --- a/packages/go_router/example/lib/state_restoration.dart +++ b/packages/go_router/example/lib/state_restoration.dart @@ -32,7 +32,6 @@ class _AppState extends State with RestorationMixin { @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 68288dd4da..204fc30a44 100644 --- a/packages/go_router/example/lib/sub_routes.dart +++ b/packages/go_router/example/lib/sub_routes.dart @@ -19,7 +19,6 @@ class App extends StatelessWidget { @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 08c8faa28d..2ccbcef9a4 100644 --- a/packages/go_router/example/lib/transitions.dart +++ b/packages/go_router/example/lib/transitions.dart @@ -17,7 +17,6 @@ class App extends StatelessWidget { @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 0bebb1b306..f3b618a367 100644 --- a/packages/go_router/example/lib/url_strategy.dart +++ b/packages/go_router/example/lib/url_strategy.dart @@ -25,7 +25,6 @@ class App extends StatelessWidget { @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 56cf9a95ae..3054cb7339 100644 --- a/packages/go_router/example/lib/user_input.dart +++ b/packages/go_router/example/lib/user_input.dart @@ -20,7 +20,6 @@ class App extends StatelessWidget { @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 cd4cc50b42..6474f3c89a 100644 --- a/packages/go_router/example/lib/widgets_app.dart +++ b/packages/go_router/example/lib/widgets_app.dart @@ -21,7 +21,6 @@ class App extends StatelessWidget { @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 04de89a20f..d5a7b1549e 100644 --- a/packages/go_router/lib/src/go_route_information_parser.dart +++ b/packages/go_router/lib/src/go_route_information_parser.dart @@ -4,435 +4,20 @@ 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> { - /// 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 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 _nameToPath = {}; - - void _cacheNameToPath(String parentFullPath, List 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 params = const {}, - Map queryParams = const {}, - }) { - 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 paramNames = []; - 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 encodedParams = { - for (final MapEntry 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'; - } - +class GoRouteInformationParser extends RouteInformationParser { /// for use by the Router architecture as part of the RouteInformationParser @override - Future> parseRouteInformation( + Future 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 matches = - _getLocRouteMatchesWithRedirects(routeInformation); - // Use [SynchronousFuture] so that the initial url is processed - // synchronously and remove unwanted initial animations on deep-linking - return SynchronousFuture>(matches); - } - - List _getLocRouteMatchesWithRedirects( - RouteInformation routeInformation) { - // start redirecting from the initial location - List matches; - final String location = routeInformation.location!; - try { - // watch redirects for loops - final List redirects = [_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: ${[ - ...redirects, - redir - ].join(' => ')}'); - } - if (redirects.length > redirectLimit) { - throw _ParserError('too many redirects: ${[ - ...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 previouslyMatchedParams = {}; - 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( - subloc: uri.path, - fullpath: uri.path, - encodedParams: {}, - queryParams: uri.queryParameters, - extra: null, - error: error, - route: GoRoute( - path: location, - pageBuilder: (BuildContext context, GoRouterState state) { - throw UnimplementedError(); - }), - ), - ]; - } - assert(matches.isNotEmpty); - return matches; - } - - List _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 _getLocRouteRecursively({ - required String loc, - required String restLoc, - required String parentSubloc, - required List routes, - required String parentFullpath, - required Map queryParams, - required Object? extra, - }) { - bool debugGatherAllMatches = false; - assert(() { - debugGatherAllMatches = true; - return true; - }()); - final List> result = >[]; - // 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([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 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([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 e in _nameToPath.entries) { - log.info(' ${e.key} => ${e.value}'); - } - } - } - - void _debugLogFullPathsFor( - List 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); - } - } + ) => + // Use [SynchronousFuture] so that the initial url is processed + // synchronously and remove unwanted initial animations on deep-linking + SynchronousFuture(Uri.parse(routeInformation.location!)); /// for use by the Router architecture as part of the RouteInformationParser @override - RouteInformation restoreRouteInformation(List 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; + RouteInformation restoreRouteInformation(Uri configuration) => + RouteInformation(location: configuration.toString()); } diff --git a/packages/go_router/lib/src/go_route_information_provider.dart b/packages/go_router/lib/src/go_route_information_provider.dart deleted file mode 100644 index 549f6b6d75..0000000000 --- a/packages/go_router/lib/src/go_route_information_provider.dart +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package: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 didPushRouteInformation( - RouteInformation routeInformation) async { - assert(hasListeners); - print('_platformReportsNewRouteInformation $routeInformation'); - _platformReportsNewRouteInformation(routeInformation); - return true; - } - - @override - Future 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 35a03aa9c6..345aa5dfcc 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_route_information_parser.dart'; +import 'go_router_delegate.dart'; import 'path_parser.dart'; /// Each GoRouteMatch instance represents an instance of a GoRoute for a @@ -22,8 +22,7 @@ class GoRouteMatch { required this.extra, required this.error, this.pageKey, - }) : fullUriString = _addQueryParams(subloc, queryParams), - assert(subloc.startsWith('/')), + }) : assert(subloc.startsWith('/')), assert(Uri.parse(subloc).queryParameters.isEmpty), assert(fullpath.startsWith('/')), assert(Uri.parse(fullpath).queryParameters.isEmpty), @@ -35,16 +34,61 @@ class GoRouteMatch { return true; }()); + // 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 params, // e.g. {'fid': 'f2', 'pid': 'p1'} + required Map 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 paramNames = []; + 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 encodedParams = { + for (final MapEntry 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 queryParams, required Object? extra, }) { - assert(!route.path.contains('//')); + assert(!path.contains('//')); final RegExpMatch? match = route.matchPatternAsPrefix(restLoc); if (match == null) { @@ -52,9 +96,8 @@ class GoRouteMatch { } final Map encodedParams = route.extractPathParams(match); - final String pathLoc = patternToPath(route.path, encodedParams); - final String subloc = - GoRouteInformationParser.concatenatePaths(parentSubloc, pathLoc); + final String pathLoc = _locationFor(path, encodedParams); + final String subloc = GoRouterDelegate.fullLocFor(parentSubloc, pathLoc); return GoRouteMatch( route: route, subloc: subloc, @@ -90,18 +133,6 @@ class GoRouteMatch { /// Optional value key of type string, to hold a unique reference to a page. final ValueKey? pageKey; - /// The full uri string - final String fullUriString; // e.g. /family/12?query=14 - - static String _addQueryParams(String loc, Map 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 get decodedParams => { for (final MapEntry param in encodedParams.entries) @@ -111,4 +142,8 @@ class GoRouteMatch { /// 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 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 b3e4c3f814..7ed7425f19 100644 --- a/packages/go_router/lib/src/go_router.dart +++ b/packages/go_router/lib/src/go_router.dart @@ -6,8 +6,6 @@ import 'package:flutter/widgets.dart'; 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'; @@ -32,7 +30,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { Listenable? refreshListenable, int redirectLimit = 5, bool routerNeglect = false, - String? initialLocation, + String initialLocation = '/', UrlPathStrategy? urlPathStrategy, List? observers, bool debugLogDiagnostics = false, @@ -44,31 +42,21 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { } setLogging(enabled: debugLogDiagnostics); - WidgetsFlutterBinding.ensureInitialized(); - - final String _effectiveInitialLocation = initialLocation ?? - // ignore: unnecessary_non_null_assertion - WidgetsBinding.instance!.platformDispatcher.defaultRouteName; - routeInformationParser = GoRouteInformationParser( - routes: routes, - topRedirect: redirect ?? (_) => null, - redirectLimit: redirectLimit, - debugRequireGoRouteInformationProvider: true, - ); - routeInformationProvider = GoRouteInformationProvider( - initialRouteInformation: - RouteInformation(location: _effectiveInitialLocation), - refreshListenable: refreshListenable); routerDelegate = GoRouterDelegate( - routeInformationParser, + routes: routes, errorPageBuilder: errorPageBuilder, errorBuilder: errorBuilder, + topRedirect: redirect ?? (_) => null, + redirectLimit: redirectLimit, + refreshListenable: refreshListenable, routerNeglect: routerNeglect, + initUri: Uri.parse(initialLocation), observers: [ ...observers ?? [], 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 @@ -79,23 +67,17 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { 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. - late final GoRouteInformationParser routeInformationParser; + final GoRouteInformationParser routeInformationParser = + GoRouteInformationParser(); /// 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.last.fullUriString; + String get location => routerDelegate.currentConfiguration.toString(); /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. @@ -104,7 +86,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { Map params = const {}, Map queryParams = const {}, }) => - routeInformationParser.namedLocation( + routerDelegate.namedLocation( name, params: params, queryParams: queryParams, @@ -112,14 +94,8 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// Navigate to a URI location w/ optional query parameters, e.g. /// `/family/f2/person/p1?color=blue` - void go(String location, {Object? extra}) { - assert(() { - log.info('going to $location'); - return true; - }()); - routeInformationProvider.value = - RouteInformation(location: location, state: extra); - } + void go(String location, {Object? extra}) => + routerDelegate.go(location, extra: extra); /// Navigate to a named route w/ optional parameters, e.g. /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` @@ -137,18 +113,8 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { /// 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}) { - assert(() { - log.info('pushing $location'); - return true; - }()); - routeInformationParser - .parseRouteInformation( - DebugGoRouteInformation(location: location, state: extra)) - .then((List matches) { - routerDelegate.push(matches.last); - }); - } + void push(String location, {Object? extra}) => + routerDelegate.push(location, extra: extra); /// Push a named route onto the page stack w/ optional parameters, e.g. /// `name='person', params={'fid': 'f2', 'pid': 'p1'}` @@ -167,13 +133,7 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { void pop() => routerDelegate.pop(); /// Refresh the route. - void refresh() { - assert(() { - log.info('refreshing $location'); - return true; - }()); - routeInformationProvider.notifyListeners(); - } + void refresh() => routerDelegate.refresh(); /// Set the app's URL path strategy (defaults to hash). call before runApp(). static void setUrlPathStrategy(UrlPathStrategy strategy) => @@ -206,11 +166,4 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { @override void didReplace({Route? newRoute, Route? 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 fed213ceca..f901a94de1 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/foundation.dart'; import 'package:flutter/widgets.dart'; import 'custom_transition_page.dart'; -import 'go_route_information_parser.dart'; +import 'go_route.dart'; import 'go_route_match.dart'; import 'go_router_cupertino.dart'; import 'go_router_error_page.dart'; @@ -19,36 +19,76 @@ import 'route_data.dart'; import 'typedefs.dart'; /// GoRouter implementation of the RouterDelegate base class. -class GoRouterDelegate extends RouterDelegate> - with PopNavigatorRouterDelegateMixin>, ChangeNotifier { +class GoRouterDelegate extends RouterDelegate + with + PopNavigatorRouterDelegateMixin, + // ignore: prefer_mixin + ChangeNotifier { /// Constructor for GoRouter's implementation of the /// RouterDelegate base class. - GoRouterDelegate( - this._parser, { + GoRouterDelegate({ 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); - // TODO(chunhtai): remove this once namedLocation is removed from go_router. - final GoRouteInformationParser _parser; + // 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); + } /// Builder function for a go router with Navigator. final GoRouterBuilderWithNav builderWithNav; + /// List of top level routes used by the go router delegate. + final List 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 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; @@ -57,11 +97,78 @@ class GoRouterDelegate extends RouterDelegate> final String? restorationScopeId; final GlobalKey _key = GlobalKey(); - List _matches = const []; + final List _matches = []; + final Map _namedMatches = {}; + final Map _pushCounts = {}; + + void _cacheNamedRoutes( + List routes, + String parentFullpath, + Map 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: {}, + queryParams: {}, + 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 params, + required Map 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(); + } /// Push the given location onto the page stack - void push(GoRouteMatch match) { - _matches.add(match); + void push(String location, {Object? extra}) { + log.info('pushing $location'); + _push(location, extra: extra); notifyListeners(); } @@ -73,17 +180,35 @@ class GoRouterDelegate extends RouterDelegate> 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 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 get navigatorKey => _key; /// For use by the Router architecture as part of the RouterDelegate. @override - List get currentConfiguration => _matches; + Uri get currentConfiguration => Uri.parse(location); /// For use by the Router architecture as part of the RouterDelegate. @override @@ -91,17 +216,394 @@ class GoRouterDelegate extends RouterDelegate> /// For use by the Router architecture as part of the RouterDelegate. @override - Future setNewRoutePath(List configuration) { - _matches = configuration; + Future 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); + } + // Use [SynchronousFuture] so that the initial url is processed // synchronously and remove unwanted initial animations on deep-linking return SynchronousFuture(null); } + /// For use by the Router architecture as part of the RouterDelegate. + @override + Future setNewRoutePath(Uri configuration) async { + final String config = configuration.toString(); + log.info('going to $config'); + _go(config); + } + + void _go(String location, {Object? extra}) { + final List 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 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 pageKey = ValueKey('$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 _getLocRouteMatchesWithRedirects( + String location, { + required Object? extra, + }) { + // start redirecting from the initial location + List matches; + + try { + // watch redirects for loops + final List redirects = [_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: ${[ + ...redirects, + redir + ].join(' => ')}'); + assert( + redirects.length < redirectLimit, + 'too many redirects: ${[ + ...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 previouslyMatchedParams = {}; + 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( + subloc: uri.path, + fullpath: uri.path, + encodedParams: {}, + 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 _getLocRouteMatches( + String location, { + Object? extra, + }) { + final Uri uri = Uri.parse(location); + final List> 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: [ + /// / + /// 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> _getLocRouteMatchStacks({ + required String loc, + required String restLoc, + required String parentSubloc, + required List routes, + required String parentFullpath, + required Map 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 [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> 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 stack in subRouteMatchStacks) { + yield [match, ...stack]; + } + } + } + + GoRouteMatch? _getNameRouteMatch( + String name, { + Map params = const {}, + Map queryParams = const {}, + 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 matches) { List>? pages; Exception? error; - final String location = matches.last.fullUriString; + try { // build the stack of pages if (routerNeglect) { @@ -118,10 +620,7 @@ class GoRouterDelegate extends RouterDelegate> // `redirect` impl // ignore: avoid_catches_without_on_clauses } catch (err, stack) { - assert(() { - log.severe('Exception during GoRouter navigation', err, stack); - return true; - }()); + log.severe('Exception during GoRouter navigation', err, stack); // if there's an error, show an error page error = err is Exception ? err : Exception(err); @@ -130,7 +629,7 @@ class GoRouterDelegate extends RouterDelegate> _errorPageBuilder( context, GoRouterState( - _parser, + this, location: location, subloc: uri.path, name: null, @@ -155,7 +654,7 @@ class GoRouterDelegate extends RouterDelegate> return builderWithNav( context, GoRouterState( - _parser, + this, location: location, name: null, // no name available at the top level // trim the query params off the subloc to match route.redirect @@ -216,24 +715,20 @@ class GoRouterDelegate extends RouterDelegate> // get a page from the builder and associate it with a sub-location final GoRouterState state = GoRouterState( - _parser, - location: match.fullUriString, + this, + location: location, 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? page; if (pageBuilder != null) { page = pageBuilder(context, state); @@ -268,26 +763,17 @@ class GoRouterDelegate extends RouterDelegate> final Element? elem = context is Element ? context : null; if (elem != null && isMaterialApp(elem)) { - assert(() { - log.info('MaterialApp found'); - return true; - }()); + log.info('MaterialApp found'); _pageBuilderForAppType = pageBuilderForMaterialApp; _errorBuilderForAppType = (BuildContext c, GoRouterState s) => GoRouterMaterialErrorScreen(s.error); } else if (elem != null && isCupertinoApp(elem)) { - assert(() { - log.info('CupertinoApp found'); - return true; - }()); + log.info('CupertinoApp found'); _pageBuilderForAppType = pageBuilderForCupertinoApp; _errorBuilderForAppType = (BuildContext c, GoRouterState s) => GoRouterCupertinoErrorScreen(s.error); } else { - assert(() { - log.info('WidgetsApp found'); - return true; - }()); + log.info('WidgetsApp assumed'); _pageBuilderForAppType = pageBuilderForWidgetApp; _errorBuilderForAppType = (BuildContext c, GoRouterState s) => GoRouterErrorScreen(s.error); @@ -349,4 +835,54 @@ class GoRouterDelegate extends RouterDelegate> 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 e in _namedMatches.entries) { + log.info(' ${e.key} => ${e.value.fullpath}'); + } + } + } + + void _outputFullPathsFor( + List 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 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 9db78342fd..ffcb072900 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_route_information_parser.dart'; +import 'go_router_delegate.dart'; /// The route state during routing. class GoRouterState { @@ -29,8 +29,7 @@ class GoRouterState { : subloc), assert((path ?? '').isEmpty == (fullpath ?? '').isEmpty); - // TODO(chunhtai): remove this once namedLocation is removed from go_router. - final GoRouteInformationParser _delegate; + final GoRouterDelegate _delegate; /// The full location of the route, e.g. /family/f2/person/p1 final String location; @@ -68,8 +67,6 @@ class GoRouterState { String name, { Map params = const {}, Map queryParams = const {}, - }) { - return _delegate.namedLocation(name, - params: params, queryParams: queryParams); - } + }) => + _delegate.namedLocation(name, params: params, queryParams: queryParams); } diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 0743dad434..552c4313a8 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,12 +1,13 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 4.0.0 +version: 3.1.1 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 28c622ae5d..98ac8cd492 100644 --- a/packages/go_router/test/custom_transition_page_test.dart +++ b/packages/go_router/test/custom_transition_page_test.dart @@ -24,7 +24,6 @@ void main() { ); 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 c33d3282ae..c8ae740311 100644 --- a/packages/go_router/test/error_screen_helpers.dart +++ b/packages/go_router/test/error_screen_helpers.dart @@ -51,7 +51,6 @@ WidgetTesterCallback testClickingTheButtonRedirectsToRoot({ Widget materialAppRouterBuilder(GoRouter router) { return MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, routeInformationParser: router.routeInformationParser, routerDelegate: router.routerDelegate, title: 'GoRouter Example', @@ -60,7 +59,6 @@ Widget materialAppRouterBuilder(GoRouter router) { 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 deleted file mode 100644 index 426d5846ea..0000000000 --- a/packages/go_router/test/go_route_information_parser_test.dart +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package: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 routes = [ - GoRoute( - path: '/', - builder: (_, __) => const Placeholder(), - routes: [ - GoRoute( - path: 'abc', - builder: (_, __) => const Placeholder(), - ), - ], - ), - ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (_) => null, - ); - - List 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 routes = [ - GoRoute( - path: '/', - builder: (_, __) => const Placeholder(), - routes: [ - GoRoute( - path: 'abc', - builder: (_, __) => const Placeholder(), - ), - ], - ), - ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (_) => null, - ); - - final List 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 routes = [ - GoRoute( - path: '/', - builder: (_, __) => const Placeholder(), - routes: [ - GoRoute( - path: ':uid/family/:fid', - builder: (_, __) => const Placeholder(), - ), - ], - ), - ]; - final GoRouteInformationParser parser = GoRouteInformationParser( - routes: routes, - redirectLimit: 100, - topRedirect: (_) => null, - ); - - final List 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 routes = [ - GoRoute( - path: '/', - builder: (_, __) => const Placeholder(), - routes: [ - 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 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 routes = [ - GoRoute( - path: '/', - builder: (_, __) => const Placeholder(), - routes: [ - 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 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 9ad9745196..a7153327bf 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/material.dart'; 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'; -Future createGoRouter( - WidgetTester tester, { +GoRouterDelegate createGoRouterDelegate({ Listenable? refreshListenable, -}) async { +}) { final GoRouter router = GoRouter( initialLocation: '/', routes: [ @@ -23,32 +23,26 @@ Future createGoRouter( ], refreshListenable: refreshListenable, ); - await tester.pumpWidget(MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, - routeInformationParser: router.routeInformationParser, - routerDelegate: router.routerDelegate)); - return router; + return router.routerDelegate; } void main() { group('pop', () { - 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('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('throws when it pops more than matches count', - (WidgetTester tester) async { - final GoRouter goRouter = await createGoRouter(tester) + test('throws when it pops more than matches count', () { + final GoRouterDelegate delegate = createGoRouterDelegate() ..push('/error'); expect( - () => goRouter.routerDelegate + () => delegate ..pop() ..pop(), throwsA(isAssertionError), @@ -56,13 +50,9 @@ void main() { }); }); - testWidgets('dispose unsubscribes from refreshListenable', - (WidgetTester tester) async { + test('dispose unsubscribes from refreshListenable', () { final FakeRefreshListenable refreshListenable = FakeRefreshListenable(); - final GoRouter goRouter = - await createGoRouter(tester, refreshListenable: refreshListenable); - await tester.pumpWidget(Container()); - goRouter.dispose(); + createGoRouterDelegate(refreshListenable: refreshListenable).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 ca6461b963..8316597458 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -24,7 +24,7 @@ void main() { Logger.root.onRecord.listen((LogRecord e) => debugPrint('$e')); group('path routes', () { - testWidgets('match home route', (WidgetTester tester) async { + test('match home route', () { final List routes = [ GoRoute( path: '/', @@ -32,21 +32,20 @@ void main() { const HomeScreen()), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); }); - testWidgets('If there is more than one route to match, use the first match', - (WidgetTester tester) async { + test('If there is more than one route to match, use the first match', () { final List routes = [ GoRoute(path: '/', builder: _dummy), GoRoute(path: '/', builder: _dummy), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/'); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); @@ -90,30 +89,28 @@ void main() { }, throwsA(isAssertionError)); }); - testWidgets('lack of leading / on top-level route', - (WidgetTester tester) async { - await expectLater(() async { + test('lack of leading / on top-level route', () { + expect(() { final List routes = [ GoRoute(path: 'foo', builder: _dummy), ]; - await _router(routes, tester); + _router(routes); }, throwsA(isAssertionError)); }); - testWidgets('match no routes', (WidgetTester tester) async { + test('match no routes', () { final List routes = [ GoRoute(path: '/', builder: _dummy), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/foo'); - await tester.pumpAndSettle(); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); expect(router.screenFor(matches.first).runtimeType, ErrorScreen); }); - testWidgets('match 2nd top level route', (WidgetTester tester) async { + test('match 2nd top level route', () { final List routes = [ GoRoute( path: '/', @@ -125,7 +122,7 @@ void main() { const LoginScreen()), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/login'); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); @@ -133,8 +130,7 @@ void main() { expect(router.screenFor(matches.first).runtimeType, LoginScreen); }); - testWidgets('match top level route when location has trailing /', - (WidgetTester tester) async { + test('match top level route when location has trailing /', () { final List routes = [ GoRoute( path: '/', @@ -148,7 +144,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/login/'); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); @@ -156,14 +152,13 @@ void main() { expect(router.screenFor(matches.first).runtimeType, LoginScreen); }); - testWidgets('match top level route when location has trailing / (2)', - (WidgetTester tester) async { + test('match top level route when location has trailing / (2)', () { final List routes = [ GoRoute(path: '/profile', redirect: (_) => '/profile/foo'), GoRoute(path: '/profile/:kind', builder: _dummy), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/profile/'); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); @@ -171,14 +166,13 @@ void main() { expect(router.screenFor(matches.first).runtimeType, DummyScreen); }); - testWidgets('match top level route when location has trailing / (3)', - (WidgetTester tester) async { + test('match top level route when location has trailing / (3)', () { final List routes = [ GoRoute(path: '/profile', redirect: (_) => '/profile/foo'), GoRoute(path: '/profile/:kind', builder: _dummy), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/profile/?bar=baz'); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); @@ -186,7 +180,7 @@ void main() { expect(router.screenFor(matches.first).runtimeType, DummyScreen); }); - testWidgets('match sub-route', (WidgetTester tester) async { + test('match sub-route', () { final List routes = [ GoRoute( path: '/', @@ -202,7 +196,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/login'); final List matches = router.routerDelegate.matches; expect(matches.length, 2); @@ -212,7 +206,7 @@ void main() { expect(router.screenFor(matches[1]).runtimeType, LoginScreen); }); - testWidgets('match sub-routes', (WidgetTester tester) async { + test('match sub-routes', () { final List routes = [ GoRoute( path: '/', @@ -240,7 +234,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); { final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); @@ -281,8 +275,7 @@ void main() { } }); - testWidgets('return first matching route if too many subroutes', - (WidgetTester tester) async { + test('return first matching route if too many subroutes', () { final List routes = [ GoRoute( path: '/', @@ -315,7 +308,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/bar'); List matches = router.routerDelegate.matches; expect(matches, hasLength(2)); @@ -332,12 +325,28 @@ void main() { expect(router.screenFor(matches[1]).runtimeType, Page2Screen); }); - testWidgets('router state', (WidgetTester tester) async { + test('router state', () { final List routes = [ GoRoute( name: 'home', path: '/', builder: (BuildContext context, GoRouterState state) { + expect( + state.location, + anyOf([ + '/', + '/login', + '/family/f2', + '/family/f2/person/p1' + ]), + ); + expect(state.subloc, '/'); + expect(state.name, 'home'); + expect(state.path, '/'); + expect(state.fullpath, '/'); + expect(state.params, {}); + expect(state.error, null); + expect(state.extra! as int, 1); return const HomeScreen(); }, routes: [ @@ -399,18 +408,14 @@ void main() { ), ]; - 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(); + 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); }); - testWidgets('match path case insensitively', (WidgetTester tester) async { + test('match path case insensitively', () { final List routes = [ GoRoute( path: '/', @@ -424,7 +429,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); const String loc = '/FaMiLy/f2'; router.go(loc); final List matches = router.routerDelegate.matches; @@ -438,9 +443,7 @@ void main() { expect(router.screenFor(matches.first).runtimeType, FamilyScreen); }); - testWidgets( - 'If there is more than one route to match, use the first match.', - (WidgetTester tester) async { + test('If there is more than one route to match, use the first match.', () { final List routes = [ GoRoute(path: '/', builder: _dummy), GoRoute(path: '/page1', builder: _dummy), @@ -448,7 +451,7 @@ void main() { GoRoute(path: '/:ok', builder: _dummy), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/user'); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); @@ -457,7 +460,7 @@ void main() { }); group('named routes', () { - testWidgets('match home route', (WidgetTester tester) async { + test('match home route', () { final List routes = [ GoRoute( name: 'home', @@ -466,18 +469,18 @@ void main() { const HomeScreen()), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.goNamed('home'); }); - testWidgets('match too many routes', (WidgetTester tester) async { + test('match too many routes', () { final List routes = [ GoRoute(name: 'home', path: '/', builder: _dummy), GoRoute(name: 'home', path: '/', builder: _dummy), ]; - await expectLater(() async { - await _router(routes, tester); + expect(() { + _router(routes); }, throwsA(isAssertionError)); }); @@ -487,17 +490,17 @@ void main() { }, throwsA(isAssertionError)); }); - testWidgets('match no routes', (WidgetTester tester) async { - await expectLater(() async { + test('match no routes', () { + expect(() { final List routes = [ GoRoute(name: 'home', path: '/', builder: _dummy), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.goNamed('work'); }, throwsA(isAssertionError)); }); - testWidgets('match 2nd top level route', (WidgetTester tester) async { + test('match 2nd top level route', () { final List routes = [ GoRoute( name: 'home', @@ -513,11 +516,11 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.goNamed('login'); }); - testWidgets('match sub-route', (WidgetTester tester) async { + test('match sub-route', () { final List routes = [ GoRoute( name: 'home', @@ -535,11 +538,40 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.goNamed('login'); }); - testWidgets('match w/ params', (WidgetTester tester) async { + test('match sub-route case insensitive', () { + final List routes = [ + GoRoute( + name: 'home', + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + 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', () { final List routes = [ GoRoute( name: 'home', @@ -568,12 +600,12 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.goNamed('person', params: {'fid': 'f2', 'pid': 'p1'}); }); - testWidgets('too few params', (WidgetTester tester) async { + test('too few params', () { final List routes = [ GoRoute( name: 'home', @@ -598,15 +630,13 @@ void main() { ], ), ]; - await expectLater(() async { - final GoRouter router = await _router(routes, tester); + expect(() { + final GoRouter router = _router(routes); router.goNamed('person', params: {'fid': 'f2'}); - await tester.pump(); }, throwsA(isAssertionError)); }); - testWidgets('match case insensitive w/ params', - (WidgetTester tester) async { + test('match case insensitive w/ params', () { final List routes = [ GoRoute( name: 'home', @@ -635,12 +665,12 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.goNamed('person', params: {'fid': 'f2', 'pid': 'p1'}); }); - testWidgets('too few params', (WidgetTester tester) async { + test('too few params', () { final List routes = [ GoRoute( name: 'family', @@ -649,13 +679,13 @@ void main() { const FamilyScreen('dummy'), ), ]; - await expectLater(() async { - final GoRouter router = await _router(routes, tester); + expect(() { + final GoRouter router = _router(routes); router.goNamed('family'); }, throwsA(isAssertionError)); }); - testWidgets('too many params', (WidgetTester tester) async { + test('too many params', () { final List routes = [ GoRoute( name: 'family', @@ -664,14 +694,14 @@ void main() { const FamilyScreen('dummy'), ), ]; - await expectLater(() async { - final GoRouter router = await _router(routes, tester); + expect(() { + final GoRouter router = _router(routes); router.goNamed('family', params: {'fid': 'f2', 'pid': 'p1'}); }, throwsA(isAssertionError)); }); - testWidgets('sparsely named routes', (WidgetTester tester) async { + test('sparsely named routes', () { final List routes = [ GoRoute( path: '/', @@ -696,7 +726,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.goNamed('person', params: {'fid': 'f2', 'pid': 'p1'}); @@ -704,8 +734,7 @@ void main() { expect(router.screenFor(matches.last).runtimeType, PersonScreen); }); - testWidgets('preserve path param spaces and slashes', - (WidgetTester tester) async { + test('preserve path param spaces and slashes', () { const String param1 = 'param w/ spaces and slashes'; final List routes = [ GoRoute( @@ -718,7 +747,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); final String loc = router .namedLocation('page1', params: {'param1': param1}); log.info('loc= $loc'); @@ -730,8 +759,7 @@ void main() { expect(matches.first.decodedParams['param1'], param1); }); - testWidgets('preserve query param spaces and slashes', - (WidgetTester tester) async { + test('preserve query param spaces and slashes', () { const String param1 = 'param w/ spaces and slashes'; final List routes = [ GoRoute( @@ -744,19 +772,21 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); final String loc = router.namedLocation('page1', queryParams: {'param1': param1}); + log.info('loc= $loc'); router.go(loc); - await tester.pump(); + final List 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', () { - testWidgets('top-level redirect', (WidgetTester tester) async { + test('top-level redirect', () { final List routes = [ GoRoute( path: '/', @@ -775,15 +805,16 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester, - redirect: (GoRouterState state) => - state.subloc == '/login' ? null : '/login'); - + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, + redirect: (GoRouterState state) => + state.subloc == '/login' ? null : '/login', + ); expect(router.location, '/login'); }); - testWidgets('top-level redirect w/ named routes', - (WidgetTester tester) async { + test('top-level redirect w/ named routes', () { final List routes = [ GoRoute( name: 'home', @@ -807,16 +838,17 @@ void main() { ), ]; - final GoRouter router = await _router( - routes, - tester, + final GoRouter router = GoRouter( + debugLogDiagnostics: true, + routes: routes, + errorBuilder: _dummy, redirect: (GoRouterState state) => state.subloc == '/login' ? null : state.namedLocation('login'), ); expect(router.location, '/login'); }); - testWidgets('route-level redirect', (WidgetTester tester) async { + test('route-level redirect', () { final List routes = [ GoRoute( path: '/', @@ -838,14 +870,15 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, + ); router.go('/dummy'); - await tester.pump(); expect(router.location, '/login'); }); - testWidgets('route-level redirect w/ named routes', - (WidgetTester tester) async { + test('route-level redirect w/ named routes', () { final List routes = [ GoRoute( name: 'home', @@ -870,13 +903,15 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, + ); router.go('/dummy'); - await tester.pump(); expect(router.location, '/login'); }); - testWidgets('multiple mixed redirect', (WidgetTester tester) async { + test('multiple mixed redirect', () { final List routes = [ GoRoute( path: '/', @@ -898,42 +933,26 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester, - redirect: (GoRouterState state) => - state.subloc == '/dummy1' ? '/dummy2' : null); + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, + redirect: (GoRouterState state) => + state.subloc == '/dummy1' ? '/dummy2' : null, + ); router.go('/dummy1'); - await tester.pump(); expect(router.location, '/'); }); - testWidgets('top-level redirect loop', (WidgetTester tester) async { - final GoRouter router = await _router([], tester, - redirect: (GoRouterState state) => state.subloc == '/' - ? '/login' - : state.subloc == '/login' - ? '/' - : null); - - final List matches = router.routerDelegate.matches; - expect(matches, hasLength(1)); - expect(router.screenFor(matches.first).runtimeType, ErrorScreen); - expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); - log.info((router.screenFor(matches.first) as ErrorScreen).ex); - }); - - testWidgets('route-level redirect loop', (WidgetTester tester) async { - final GoRouter router = await _router( - [ - GoRoute( - path: '/', - redirect: (GoRouterState state) => '/login', - ), - GoRoute( - path: '/login', - redirect: (GoRouterState state) => '/', - ), - ], - tester, + test('top-level redirect loop', () { + final GoRouter router = GoRouter( + routes: [], + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), + redirect: (GoRouterState state) => state.subloc == '/' + ? '/login' + : state.subloc == '/login' + ? '/' + : null, ); final List matches = router.routerDelegate.matches; @@ -943,15 +962,39 @@ void main() { log.info((router.screenFor(matches.first) as ErrorScreen).ex); }); - testWidgets('mixed redirect loop', (WidgetTester tester) async { - final GoRouter router = await _router( - [ + test('route-level redirect loop', () { + final GoRouter router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (GoRouterState state) => '/login', + ), GoRoute( path: '/login', redirect: (GoRouterState state) => '/', ), ], - tester, + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), + ); + + final List matches = router.routerDelegate.matches; + expect(matches, hasLength(1)); + expect(router.screenFor(matches.first).runtimeType, ErrorScreen); + expect((router.screenFor(matches.first) as ErrorScreen).ex, isNotNull); + log.info((router.screenFor(matches.first) as ErrorScreen).ex); + }); + + test('mixed redirect loop', () { + final GoRouter router = GoRouter( + routes: [ + GoRoute( + path: '/login', + redirect: (GoRouterState state) => '/', + ), + ], + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), redirect: (GoRouterState state) => state.subloc == '/' ? '/login' : null, ); @@ -963,11 +1006,11 @@ void main() { log.info((router.screenFor(matches.first) as ErrorScreen).ex); }); - testWidgets('top-level redirect loop w/ query params', - (WidgetTester tester) async { - final GoRouter router = await _router( - [], - tester, + test('top-level redirect loop w/ query params', () { + final GoRouter router = GoRouter( + routes: [], + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), redirect: (GoRouterState state) => state.subloc == '/' ? '/login?from=${state.location}' : state.subloc == '/login' @@ -982,8 +1025,7 @@ void main() { log.info((router.screenFor(matches.first) as ErrorScreen).ex); }); - testWidgets('expect null path/fullpath on top-level redirect', - (WidgetTester tester) async { + test('expect null path/fullpath on top-level redirect', () { final List routes = [ GoRoute( path: '/', @@ -996,15 +1038,15 @@ void main() { ), ]; - final GoRouter router = await _router( - routes, - tester, + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, initialLocation: '/dummy', ); expect(router.location, '/'); }); - testWidgets('top-level redirect state', (WidgetTester tester) async { + test('top-level redirect state', () { final List routes = [ GoRoute( path: '/', @@ -1018,10 +1060,11 @@ void main() { ), ]; - final GoRouter router = await _router( - routes, - tester, + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, initialLocation: '/login?from=/', + debugLogDiagnostics: true, redirect: (GoRouterState state) { expect(Uri.parse(state.location).queryParameters, isNotEmpty); expect(Uri.parse(state.subloc).queryParameters, isEmpty); @@ -1039,7 +1082,7 @@ void main() { expect(router.screenFor(matches.first).runtimeType, LoginScreen); }); - testWidgets('route-level redirect state', (WidgetTester tester) async { + test('route-level redirect state', () { const String loc = '/book/0'; final List routes = [ GoRoute( @@ -1057,10 +1100,11 @@ void main() { ), ]; - final GoRouter router = await _router( - routes, - tester, + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, initialLocation: loc, + debugLogDiagnostics: true, ); final List matches = router.routerDelegate.matches; @@ -1068,8 +1112,7 @@ void main() { expect(router.screenFor(matches.first).runtimeType, HomeScreen); }); - testWidgets('sub-sub-route-level redirect params', - (WidgetTester tester) async { + test('sub-sub-route-level redirect params', () { final List routes = [ GoRoute( path: '/', @@ -1098,10 +1141,11 @@ void main() { ), ]; - final GoRouter router = await _router( - routes, - tester, + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, initialLocation: '/family/f2/person/p1', + debugLogDiagnostics: true, ); final List matches = router.routerDelegate.matches; @@ -1113,10 +1157,12 @@ void main() { expect(page.pid, 'p1'); }); - testWidgets('redirect limit', (WidgetTester tester) async { - final GoRouter router = await _router( - [], - tester, + test('redirect limit', () { + final GoRouter router = GoRouter( + routes: [], + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), + debugLogDiagnostics: true, redirect: (GoRouterState state) => '${state.location}+', redirectLimit: 10, ); @@ -1130,7 +1176,7 @@ void main() { }); group('initial location', () { - testWidgets('initial location', (WidgetTester tester) async { + test('initial location', () { final List routes = [ GoRoute( path: '/', @@ -1146,15 +1192,15 @@ void main() { ), ]; - final GoRouter router = await _router( - routes, - tester, + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, initialLocation: '/dummy', ); expect(router.location, '/dummy'); }); - testWidgets('initial location w/ redirection', (WidgetTester tester) async { + test('initial location w/ redirection', () { final List routes = [ GoRoute( path: '/', @@ -1167,9 +1213,9 @@ void main() { ), ]; - final GoRouter router = await _router( - routes, - tester, + final GoRouter router = GoRouter( + routes: routes, + errorBuilder: _dummy, initialLocation: '/dummy', ); expect(router.location, '/'); @@ -1177,7 +1223,7 @@ void main() { }); group('params', () { - testWidgets('preserve path param case', (WidgetTester tester) async { + test('preserve path param case', () { final List routes = [ GoRoute( path: '/', @@ -1191,7 +1237,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); for (final String fid in ['f2', 'F2']) { final String loc = '/family/$fid'; router.go(loc); @@ -1204,7 +1250,7 @@ void main() { } }); - testWidgets('preserve query param case', (WidgetTester tester) async { + test('preserve query param case', () { final List routes = [ GoRoute( path: '/', @@ -1219,7 +1265,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); for (final String fid in ['f2', 'F2']) { final String loc = '/family?fid=$fid'; router.go(loc); @@ -1232,8 +1278,7 @@ void main() { } }); - testWidgets('preserve path param spaces and slashes', - (WidgetTester tester) async { + test('preserve path param spaces and slashes', () { const String param1 = 'param w/ spaces and slashes'; final List routes = [ GoRoute( @@ -1245,7 +1290,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); final String loc = '/page1/${Uri.encodeComponent(param1)}'; router.go(loc); @@ -1255,8 +1300,7 @@ void main() { expect(matches.first.decodedParams['param1'], param1); }); - testWidgets('preserve query param spaces and slashes', - (WidgetTester tester) async { + test('preserve query param spaces and slashes', () { const String param1 = 'param w/ spaces and slashes'; final List routes = [ GoRoute( @@ -1268,7 +1312,7 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); router.go('/page1?param1=$param1'); final List matches = router.routerDelegate.matches; @@ -1302,9 +1346,9 @@ void main() { } }); - testWidgets('duplicate query param', (WidgetTester tester) async { - final GoRouter router = await _router( - [ + test('duplicate query param', () { + final GoRouter router = GoRouter( + routes: [ GoRoute( path: '/', builder: (BuildContext context, GoRouterState state) { @@ -1316,18 +1360,20 @@ void main() { }, ), ], - tester, - initialLocation: '/?id=0&id=1', + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), ); + + router.go('/?id=0&id=1'); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); }); - testWidgets('duplicate path + query param', (WidgetTester tester) async { - final GoRouter router = await _router( - [ + test('duplicate path + query param', () { + final GoRouter router = GoRouter( + routes: [ GoRoute( path: '/:id', builder: (BuildContext context, GoRouterState state) { @@ -1337,20 +1383,19 @@ void main() { }, ), ], - tester, + errorBuilder: _dummy, ); router.go('/0?id=1'); - await tester.pump(); final List matches = router.routerDelegate.matches; expect(matches, hasLength(1)); expect(matches.first.fullpath, '/:id'); expect(router.screenFor(matches.first).runtimeType, HomeScreen); }); - testWidgets('push + query param', (WidgetTester tester) async { - final GoRouter router = await _router( - [ + test('push + query param', () { + final GoRouter router = GoRouter( + routes: [ GoRoute(path: '/', builder: _dummy), GoRoute( path: '/family', @@ -1368,13 +1413,11 @@ void main() { ), ), ], - tester, + errorBuilder: _dummy, ); 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'); @@ -1385,9 +1428,9 @@ void main() { expect(page2.pid, 'p1'); }); - testWidgets('push + extra param', (WidgetTester tester) async { - final GoRouter router = await _router( - [ + test('push + extra param', () { + final GoRouter router = GoRouter( + routes: [ GoRoute(path: '/', builder: _dummy), GoRoute( path: '/family', @@ -1405,13 +1448,11 @@ void main() { ), ), ], - tester, + errorBuilder: _dummy, ); router.go('/family', extra: {'fid': 'f2'}); - await tester.pump(); router.push('/person', extra: {'fid': 'f2', 'pid': 'p1'}); - await tester.pump(); final FamilyScreen page1 = router.screenFor(router.routerDelegate.matches.first) as FamilyScreen; expect(page1.fid, 'f2'); @@ -1422,7 +1463,7 @@ void main() { expect(page2.pid, 'p1'); }); - testWidgets('keep param in nested route', (WidgetTester tester) async { + test('keep param in nested route', () { final List routes = [ GoRoute( path: '/', @@ -1447,13 +1488,12 @@ void main() { ), ]; - final GoRouter router = await _router(routes, tester); + final GoRouter router = _router(routes); const String fid = 'f1'; const String pid = 'p2'; const String loc = '/family/$fid/person/$pid'; router.push(loc); - await tester.pump(); final List matches = router.routerDelegate.matches; expect(router.location, loc); @@ -1546,7 +1586,6 @@ void main() { GoRouterNamedLocationSpy(routes: routes); await tester.pumpWidget( MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, routeInformationParser: router.routeInformationParser, routerDelegate: router.routerDelegate, title: 'GoRouter Example', @@ -1566,7 +1605,6 @@ void main() { final GoRouterGoSpy router = GoRouterGoSpy(routes: routes); await tester.pumpWidget( MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, routeInformationParser: router.routeInformationParser, routerDelegate: router.routerDelegate, title: 'GoRouter Example', @@ -1585,7 +1623,6 @@ void main() { final GoRouterGoNamedSpy router = GoRouterGoNamedSpy(routes: routes); await tester.pumpWidget( MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, routeInformationParser: router.routeInformationParser, routerDelegate: router.routerDelegate, title: 'GoRouter Example', @@ -1608,7 +1645,6 @@ void main() { final GoRouterPushSpy router = GoRouterPushSpy(routes: routes); await tester.pumpWidget( MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, routeInformationParser: router.routeInformationParser, routerDelegate: router.routerDelegate, title: 'GoRouter Example', @@ -1627,7 +1663,6 @@ void main() { final GoRouterPushNamedSpy router = GoRouterPushNamedSpy(routes: routes); await tester.pumpWidget( MaterialApp.router( - routeInformationProvider: router.routeInformationProvider, routeInformationParser: router.routeInformationParser, routerDelegate: router.routerDelegate, title: 'GoRouter Example', @@ -1646,11 +1681,9 @@ void main() { }); 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', @@ -1661,17 +1694,20 @@ void main() { }); }); - testWidgets('pop triggers pop on routerDelegate', - (WidgetTester tester) async { - final GoRouter router = await createGoRouter(tester) - ..push('/error'); + test('pop triggers pop on routerDelegate', () { + final GoRouter router = createGoRouter()..push('/error'); router.routerDelegate.addListener(expectAsync0(() {})); router.pop(); - await tester.pump(); }); - testWidgets('didPush notifies listeners', (WidgetTester tester) async { - await createGoRouter(tester) + test('refresh triggers refresh on routerDelegate', () { + final GoRouter router = createGoRouter(); + router.routerDelegate.addListener(expectAsync0(() {})); + router.refresh(); + }); + + test('didPush notifies listeners', () { + createGoRouter() ..addListener(expectAsync0(() {})) ..didPush( MaterialPageRoute(builder: (_) => const Text('Current route')), @@ -1679,8 +1715,8 @@ void main() { ); }); - testWidgets('didPop notifies listeners', (WidgetTester tester) async { - await createGoRouter(tester) + test('didPop notifies listeners', () { + createGoRouter() ..addListener(expectAsync0(() {})) ..didPop( MaterialPageRoute(builder: (_) => const Text('Current route')), @@ -1688,8 +1724,8 @@ void main() { ); }); - testWidgets('didRemove notifies listeners', (WidgetTester tester) async { - await createGoRouter(tester) + test('didRemove notifies listeners', () { + createGoRouter() ..addListener(expectAsync0(() {})) ..didRemove( MaterialPageRoute(builder: (_) => const Text('Current route')), @@ -1697,8 +1733,8 @@ void main() { ); }); - testWidgets('didReplace notifies listeners', (WidgetTester tester) async { - await createGoRouter(tester) + test('didReplace notifies listeners', () { + createGoRouter() ..addListener(expectAsync0(() {})) ..didReplace( newRoute: MaterialPageRoute( @@ -1710,11 +1746,23 @@ void main() { ); }); - testWidgets('uses navigatorBuilder when provided', - (WidgetTester tester) async { - final Func3 navigatorBuilder = + test('uses navigatorBuilder when provided', () { + final Func3 navigationBuilder = expectAsync3(fakeNavigationBuilder); - final GoRouter router = GoRouter( + 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( initialLocation: '/', routes: [ GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()), @@ -1726,38 +1774,6 @@ void main() { navigatorBuilder: navigatorBuilder, ); - final GoRouterDelegate delegate = router.routerDelegate; - delegate.builderWithNav( - DummyBuildContext(), - GoRouterState(router.routeInformationParser, - location: '/foo', subloc: '/bar', name: 'baz'), - const Navigator(), - ); - }); -} - -Future createGoRouter( - WidgetTester tester, { - GoRouterNavigatorBuilder? navigatorBuilder, -}) async { - final GoRouter goRouter = GoRouter( - initialLocation: '/', - routes: [ - 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, @@ -1882,31 +1898,12 @@ class GoRouterRefreshStreamSpy extends GoRouterRefreshStream { } } -Future _router( - List 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; -} +GoRouter _router(List routes) => GoRouter( + routes: routes, + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), + debugLogDiagnostics: true, + ); class ErrorScreen extends DummyScreen { const ErrorScreen(this.ex, {Key? key}) : super(key: key); @@ -1949,7 +1946,7 @@ class DummyScreen extends StatelessWidget { const DummyScreen({Key? key}) : super(key: key); @override - Widget build(BuildContext context) => const Placeholder(); + Widget build(BuildContext context) => throw UnimplementedError(); } Widget _dummy(BuildContext context, GoRouterState state) => const DummyScreen(); @@ -1964,7 +1961,7 @@ extension on GoRouter { } Widget screenFor(GoRouteMatch match) => - (_pageFor(match) as MaterialPage).child; + (_pageFor(match) as NoTransitionPage).child; } class DummyBuildContext implements BuildContext {