[go_router] Refactor RouterDelegate into functional pieces (#1653)

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