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

Several thing.

1. I move all the imperative logic from RouterDelegate to RouteInformationParser, so that the imperative API can go through Router parsing pipeline. The Parser will handle modifying mutating RouteMatchList and produce the final RouteMatchList. The RouterDelegate would only focus on building the widget base on the final RouteMatchList 
2. combine RouteMatcher and Redirector with RouteConfiguration. I feel that instead of passing three class instances around, we should probably just have one class for all the route parsing related utility.
3. serialize routeMatchList and store into browser history. This way we can let backward and forward button to reflect imperative operation as well.
4. Some minor clean ups
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 4a0d29b..0bba2a2 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,6 +1,13 @@
+## 8.0.0
+
+- **BREAKING CHANGE**:
+  - Imperatively pushed GoRoute no longer change URL.
+  - Browser backward and forward button respects imperative route operations.
+- Refactors the route parsing pipeline.
+
 ## 7.1.1
 
-* Removes obsolete null checks on non-nullable values.
+- Removes obsolete null checks on non-nullable values.
 
 ## 7.1.0
 
diff --git a/packages/go_router/README.md b/packages/go_router/README.md
index eae5297..9beccbd 100644
--- a/packages/go_router/README.md
+++ b/packages/go_router/README.md
@@ -37,7 +37,8 @@
 - [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html)
 
 ## Migration guides
-- [Migrating to 7.0.0](https://docs.google.com/document/d/10Xbpifbs4E-zh6YE5akIO8raJq_m3FIXs6nUGdOspOg).
+- [Migrating to 8.0.0](https://flutter.dev/go/go-router-v8-breaking-changes).
+- [Migrating to 7.0.0](https://flutter.dev/go/go-router-v7-breaking-changes).
 - [Migrating to 6.0.0](https://flutter.dev/go/go-router-v6-breaking-changes)
 - [Migrating to 5.1.2](https://flutter.dev/go/go-router-v5-1-2-breaking-changes)
 - [Migrating to 5.0](https://flutter.dev/go/go-router-v5-breaking-changes)
diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart
index f151251..d68958c 100644
--- a/packages/go_router/lib/src/builder.dart
+++ b/packages/go_router/lib/src/builder.dart
@@ -9,8 +9,8 @@
 import 'configuration.dart';
 import 'logging.dart';
 import 'match.dart';
-import 'matching.dart';
 import 'misc/error_screen.dart';
+import 'misc/errors.dart';
 import 'pages/cupertino.dart';
 import 'pages/material.dart';
 import 'route_data.dart';
@@ -83,7 +83,7 @@
     RouteMatchList matchList,
     bool routerNeglect,
   ) {
-    if (matchList.isEmpty) {
+    if (matchList.isEmpty && !matchList.isError) {
       // The build method can be called before async redirect finishes. Build a
       // empty box until then.
       return const SizedBox.shrink();
@@ -92,18 +92,12 @@
       context,
       Builder(
         builder: (BuildContext context) {
-          try {
-            final Map<Page<Object?>, GoRouterState> newRegistry =
-                <Page<Object?>, GoRouterState>{};
-            final Widget result = tryBuild(context, matchList, routerNeglect,
-                configuration.navigatorKey, newRegistry);
-            _registry.updateRegistry(newRegistry);
-            return GoRouterStateRegistryScope(
-                registry: _registry, child: result);
-          } on _RouteBuilderError catch (e) {
-            return _buildErrorNavigator(context, e, matchList.uri,
-                onPopPageWithRouteMatch, configuration.navigatorKey);
-          }
+          final Map<Page<Object?>, GoRouterState> newRegistry =
+              <Page<Object?>, GoRouterState>{};
+          final Widget result = tryBuild(context, matchList, routerNeglect,
+              configuration.navigatorKey, newRegistry);
+          _registry.updateRegistry(newRegistry);
+          return GoRouterStateRegistryScope(registry: _registry, child: result);
         },
       ),
     );
@@ -147,28 +141,31 @@
       bool routerNeglect,
       GlobalKey<NavigatorState> navigatorKey,
       Map<Page<Object?>, GoRouterState> registry) {
-    final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
-        <GlobalKey<NavigatorState>, List<Page<Object?>>>{};
-    try {
+    final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage;
+    if (matchList.isError) {
+      keyToPage = <GlobalKey<NavigatorState>, List<Page<Object?>>>{
+        navigatorKey: <Page<Object?>>[
+          _buildErrorPage(
+              context, _buildErrorState(matchList.error!, matchList.uri)),
+        ]
+      };
+    } else {
+      keyToPage = <GlobalKey<NavigatorState>, List<Page<Object?>>>{};
       _buildRecursive(context, matchList, 0, pagePopContext, routerNeglect,
           keyToPage, navigatorKey, registry);
 
       // Every Page should have a corresponding RouteMatch.
       assert(keyToPage.values.flattened.every((Page<Object?> page) =>
           pagePopContext.getRouteMatchForPage(page) != null));
-      return keyToPage[navigatorKey]!;
-    } on _RouteBuilderError catch (e) {
-      return <Page<Object?>>[
-        _buildErrorPage(context, e, matchList.uri),
-      ];
-    } finally {
-      /// Clean up previous cache to prevent memory leak, making sure any nested
-      /// stateful shell routes for the current match list are kept.
-      final Set<Key> activeKeys = keyToPage.keys.toSet()
-        ..addAll(_nestedStatefulNavigatorKeys(matchList));
-      _goHeroCache.removeWhere(
-          (GlobalKey<NavigatorState> key, _) => !activeKeys.contains(key));
     }
+
+    /// Clean up previous cache to prevent memory leak, making sure any nested
+    /// stateful shell routes for the current match list are kept.
+    final Set<Key> activeKeys = keyToPage.keys.toSet()
+      ..addAll(_nestedStatefulNavigatorKeys(matchList));
+    _goHeroCache.removeWhere(
+        (GlobalKey<NavigatorState> key, _) => !activeKeys.contains(key));
+    return keyToPage[navigatorKey]!;
   }
 
   static Set<GlobalKey<NavigatorState>> _nestedStatefulNavigatorKeys(
@@ -200,15 +197,15 @@
     }
     final RouteMatch match = matchList.matches[startIndex];
 
-    if (match.error != null) {
-      throw _RouteBuilderError('Match error found during build phase',
-          exception: match.error);
-    }
-
     final RouteBase route = match.route;
     final GoRouterState state = buildState(matchList, match);
     Page<Object?>? page;
-    if (route is GoRoute) {
+    if (state.error != null) {
+      page = _buildErrorPage(context, state);
+      keyToPages.putIfAbsent(navigatorKey, () => <Page<Object?>>[]).add(page);
+      _buildRecursive(context, matchList, startIndex + 1, pagePopContext,
+          routerNeglect, keyToPages, navigatorKey, registry);
+    } else if (route is GoRoute) {
       page = _buildPageForGoRoute(context, state, match, route, pagePopContext);
       // If this GoRoute is for a different Navigator, add it to the
       // list of out of scope pages
@@ -284,7 +281,7 @@
       registry[page] = state;
       pagePopContext._setRouteMatchForPage(page, match);
     } else {
-      throw _RouteBuilderException('Unsupported route type $route');
+      throw GoError('Unsupported route type $route');
     }
   }
 
@@ -324,8 +321,17 @@
       name = route.name;
       path = route.path;
     }
-    final RouteMatchList effectiveMatchList =
-        match is ImperativeRouteMatch ? match.matches : matchList;
+    final RouteMatchList effectiveMatchList;
+    if (match is ImperativeRouteMatch) {
+      effectiveMatchList = match.matches;
+      if (effectiveMatchList.isError) {
+        return _buildErrorState(
+            effectiveMatchList.error!, effectiveMatchList.uri);
+      }
+    } else {
+      effectiveMatchList = matchList;
+      assert(!effectiveMatchList.isError);
+    }
     return GoRouterState(
       configuration,
       location: effectiveMatchList.uri.toString(),
@@ -335,10 +341,10 @@
       fullPath: effectiveMatchList.fullPath,
       pathParameters:
           Map<String, String>.from(effectiveMatchList.pathParameters),
-      error: match.error,
+      error: effectiveMatchList.error,
       queryParameters: effectiveMatchList.uri.queryParameters,
       queryParametersAll: effectiveMatchList.uri.queryParametersAll,
-      extra: match.extra,
+      extra: effectiveMatchList.extra,
       pageKey: match.pageKey,
     );
   }
@@ -370,7 +376,7 @@
     final GoRouterWidgetBuilder? builder = route.builder;
 
     if (builder == null) {
-      throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route');
+      throw GoError('No routeBuilder provided to GoRoute: $route');
     }
 
     return builder(context, state);
@@ -405,7 +411,7 @@
     final Widget? widget =
         route.buildWidget(context, state, shellRouteContext!);
     if (widget == null) {
-      throw _RouteBuilderError('No builder provided to ShellRoute: $route');
+      throw GoError('No builder provided to ShellRoute: $route');
     }
 
     return widget;
@@ -485,38 +491,26 @@
         child: child,
       );
 
-  /// Builds a Navigator containing an error page.
-  Widget _buildErrorNavigator(
-      BuildContext context,
-      _RouteBuilderError e,
-      Uri uri,
-      PopPageWithRouteMatchCallback onPopPage,
-      GlobalKey<NavigatorState> navigatorKey) {
-    return _buildNavigator(
-      (Route<dynamic> route, dynamic result) => onPopPage(route, result, null),
-      <Page<Object?>>[
-        _buildErrorPage(context, e, uri),
-      ],
-      navigatorKey,
-    );
-  }
-
-  /// Builds a an error page.
-  Page<void> _buildErrorPage(
-    BuildContext context,
-    _RouteBuilderError error,
+  GoRouterState _buildErrorState(
+    Exception error,
     Uri uri,
   ) {
-    final GoRouterState state = GoRouterState(
+    final String location = uri.toString();
+    return GoRouterState(
       configuration,
-      location: uri.toString(),
+      location: location,
       matchedLocation: uri.path,
       name: null,
       queryParameters: uri.queryParameters,
       queryParametersAll: uri.queryParametersAll,
-      error: Exception(error),
-      pageKey: const ValueKey<String>('error'),
+      error: error,
+      pageKey: ValueKey<String>('$location(error)'),
     );
+  }
+
+  /// Builds a an error page.
+  Page<void> _buildErrorPage(BuildContext context, GoRouterState state) {
+    assert(state.error != null);
 
     // If the error page builder is provided, use that, otherwise, if the error
     // builder is provided, wrap that in an app-specific page (for example,
@@ -556,43 +550,6 @@
   required Widget child,
 });
 
-/// An error that occurred while building the app's UI based on the route
-/// matches.
-class _RouteBuilderError extends Error {
-  /// Constructs a [_RouteBuilderError].
-  _RouteBuilderError(this.message, {this.exception});
-
-  /// The error message.
-  final String message;
-
-  /// The exception that occurred.
-  final Exception? exception;
-
-  @override
-  String toString() {
-    return '$message ${exception ?? ""}';
-  }
-}
-
-/// An error that occurred while building the app's UI based on the route
-/// matches.
-class _RouteBuilderException implements Exception {
-  /// Constructs a [_RouteBuilderException].
-  //ignore: unused_element
-  _RouteBuilderException(this.message, {this.exception});
-
-  /// The error message.
-  final String message;
-
-  /// The exception that occurred.
-  final Exception? exception;
-
-  @override
-  String toString() {
-    return '$message ${exception ?? ""}';
-  }
-}
-
 /// Context used to provide a route to page association when popping routes.
 class _PagePopContext {
   _PagePopContext._(this.onPopPageWithRouteMatch);
diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart
index 07d02d4..f2e9d84 100644
--- a/packages/go_router/lib/src/configuration.dart
+++ b/packages/go_router/lib/src/configuration.dart
@@ -2,11 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
 import 'package:flutter/widgets.dart';
 
 import 'configuration.dart';
 import 'logging.dart';
-import 'matching.dart';
+import 'match.dart';
 import 'misc/errors.dart';
 import 'path_utils.dart';
 import 'typedefs.dart';
@@ -26,8 +28,7 @@
             _debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
         assert(_debugCheckParentNavigatorKeys(
             routes, <GlobalKey<NavigatorState>>[navigatorKey])) {
-    assert(_debugCheckStatefulShellBranchDefaultLocations(
-        routes, RouteMatcher(this)));
+    assert(_debugCheckStatefulShellBranchDefaultLocations(routes));
     _cacheNameToPath('', routes);
     log.info(debugKnownRoutes());
   }
@@ -37,11 +38,13 @@
       late bool subRouteIsTopLevel;
       if (route is GoRoute) {
         if (isTopLevel) {
-          assert(route.path.startsWith('/'),
-              'top-level path must start with "/": $route');
+          if (!route.path.startsWith('/')) {
+            throw GoError('top-level path must start with "/": $route');
+          }
         } else {
-          assert(!route.path.startsWith('/') && !route.path.endsWith('/'),
-              'sub-route path may not start or end with /: $route');
+          if (route.path.startsWith('/') || route.path.endsWith('/')) {
+            throw GoError('sub-route path may not start or end with /: $route');
+          }
         }
         subRouteIsTopLevel = false;
       } else if (route is ShellRouteBase) {
@@ -62,11 +65,11 @@
         if (parentKey != null) {
           // Verify that the root navigator or a ShellRoute ancestor has a
           // matching navigator key.
-          assert(
-              allowedKeys.contains(parentKey),
-              'parentNavigatorKey $parentKey must refer to'
-              " an ancestor ShellRoute's navigatorKey or GoRouter's"
-              ' navigatorKey');
+          if (!allowedKeys.contains(parentKey)) {
+            throw GoError('parentNavigatorKey $parentKey must refer to'
+                " an ancestor ShellRoute's navigatorKey or GoRouter's"
+                ' navigatorKey');
+          }
 
           _debugCheckParentNavigatorKeys(
             route.routes,
@@ -91,10 +94,11 @@
         );
       } else if (route is StatefulShellRoute) {
         for (final StatefulShellBranch branch in route.branches) {
-          assert(
-              !allowedKeys.contains(branch.navigatorKey),
-              'StatefulShellBranch must not reuse an ancestor navigatorKey '
-              '(${branch.navigatorKey})');
+          if (allowedKeys.contains(branch.navigatorKey)) {
+            throw GoError(
+                'StatefulShellBranch must not reuse an ancestor navigatorKey '
+                '(${branch.navigatorKey})');
+          }
 
           _debugCheckParentNavigatorKeys(
             branch.routes,
@@ -131,66 +135,77 @@
 
   // Check to see that the configured initialLocation of StatefulShellBranches
   // points to a descendant route of the route branch.
-  bool _debugCheckStatefulShellBranchDefaultLocations(
-      List<RouteBase> routes, RouteMatcher matcher) {
-    try {
-      for (final RouteBase route in routes) {
-        if (route is StatefulShellRoute) {
-          for (final StatefulShellBranch branch in route.branches) {
-            if (branch.initialLocation == null) {
-              // Recursively search for the first GoRoute descendant. Will
-              // throw assertion error if not found.
-              final GoRoute? route = branch.defaultRoute;
-              final String? initialLocation =
-                  route != null ? locationForRoute(route) : null;
-              assert(
-                  initialLocation != null,
+  bool _debugCheckStatefulShellBranchDefaultLocations(List<RouteBase> routes) {
+    for (final RouteBase route in routes) {
+      if (route is StatefulShellRoute) {
+        for (final StatefulShellBranch branch in route.branches) {
+          if (branch.initialLocation == null) {
+            // Recursively search for the first GoRoute descendant. Will
+            // throw assertion error if not found.
+            final GoRoute? route = branch.defaultRoute;
+            final String? initialLocation =
+                route != null ? locationForRoute(route) : null;
+            if (initialLocation == null) {
+              throw GoError(
                   'The default location of a StatefulShellBranch must be '
                   'derivable from GoRoute descendant');
-              assert(
-                  route!.pathParameters.isEmpty,
+            }
+            if (route!.pathParameters.isNotEmpty) {
+              throw GoError(
                   'The default location of a StatefulShellBranch cannot be '
                   'a parameterized route');
-            } else {
-              final List<RouteBase> matchRoutes =
-                  matcher.findMatch(branch.initialLocation!).routes;
-              final int shellIndex = matchRoutes.indexOf(route);
-              bool matchFound = false;
-              if (shellIndex >= 0 && (shellIndex + 1) < matchRoutes.length) {
-                final RouteBase branchRoot = matchRoutes[shellIndex + 1];
-                matchFound = branch.routes.contains(branchRoot);
-              }
-              assert(
-                  matchFound,
+            }
+          } else {
+            final RouteMatchList matchList = findMatch(branch.initialLocation!);
+            if (matchList.isError) {
+              throw GoError(
+                  'initialLocation (${matchList.uri}) of StatefulShellBranch must '
+                  'be a valid location');
+            }
+            final List<RouteBase> matchRoutes = matchList.routes;
+            final int shellIndex = matchRoutes.indexOf(route);
+            bool matchFound = false;
+            if (shellIndex >= 0 && (shellIndex + 1) < matchRoutes.length) {
+              final RouteBase branchRoot = matchRoutes[shellIndex + 1];
+              matchFound = branch.routes.contains(branchRoot);
+            }
+            if (!matchFound) {
+              throw GoError(
                   'The initialLocation (${branch.initialLocation}) of '
                   'StatefulShellBranch must match a descendant route of the '
                   'branch');
             }
           }
         }
-        _debugCheckStatefulShellBranchDefaultLocations(route.routes, matcher);
       }
-    } on MatcherError catch (e) {
-      assert(
-          false,
-          'initialLocation (${e.location}) of StatefulShellBranch must '
-          'be a valid location');
+      _debugCheckStatefulShellBranchDefaultLocations(route.routes);
     }
     return true;
   }
 
+  /// The match used when there is an error during parsing.
+  static RouteMatchList _errorRouteMatchList(Uri uri, String errorMessage) {
+    final Exception error = Exception(errorMessage);
+    return RouteMatchList(
+      matches: const <RouteMatch>[],
+      error: error,
+      uri: uri,
+      pathParameters: const <String, String>{},
+    );
+  }
+
   /// The list of top level routes used by [GoRouterDelegate].
   final List<RouteBase> routes;
 
   /// The limit for the number of consecutive redirects.
   final int redirectLimit;
 
+  /// The global key for top level navigator.
+  final GlobalKey<NavigatorState> navigatorKey;
+
   /// Top level page redirect.
   final GoRouterRedirect topRedirect;
 
-  /// The key to use when building the root [Navigator].
-  final GlobalKey<NavigatorState> navigatorKey;
-
   final Map<String, String> _nameToPath = <String, String>{};
 
   /// Looks up the url location by a [GoRoute]'s name.
@@ -235,6 +250,269 @@
         .toString();
   }
 
+  /// Finds the routes that matched the given URL.
+  RouteMatchList findMatch(String location, {Object? extra}) {
+    final Uri uri = Uri.parse(canonicalUri(location));
+
+    final Map<String, String> pathParameters = <String, String>{};
+    final List<RouteMatch>? matches = _getLocRouteMatches(uri, pathParameters);
+
+    if (matches == null) {
+      return _errorRouteMatchList(uri, 'no routes for location: $uri');
+    }
+    return RouteMatchList(
+        matches: matches,
+        uri: uri,
+        pathParameters: pathParameters,
+        extra: extra);
+  }
+
+  List<RouteMatch>? _getLocRouteMatches(
+      Uri uri, Map<String, String> pathParameters) {
+    final List<RouteMatch>? result = _getLocRouteRecursively(
+      location: uri.path,
+      remainingLocation: uri.path,
+      matchedLocation: '',
+      pathParameters: pathParameters,
+      routes: routes,
+    );
+    return result;
+  }
+
+  List<RouteMatch>? _getLocRouteRecursively({
+    required String location,
+    required String remainingLocation,
+    required String matchedLocation,
+    required Map<String, String> pathParameters,
+    required List<RouteBase> routes,
+  }) {
+    List<RouteMatch>? result;
+    late Map<String, String> subPathParameters;
+    // find the set of matches at this level of the tree
+    for (final RouteBase route in routes) {
+      subPathParameters = <String, String>{};
+
+      final RouteMatch? match = RouteMatch.match(
+        route: route,
+        remainingLocation: remainingLocation,
+        matchedLocation: matchedLocation,
+        pathParameters: subPathParameters,
+      );
+
+      if (match == null) {
+        continue;
+      }
+
+      if (match.route is GoRoute &&
+          match.matchedLocation.toLowerCase() == location.toLowerCase()) {
+        // If it is a complete match, then return the matched route
+        // NOTE: need a lower case match because matchedLocation is canonicalized to match
+        // the path case whereas the location can be of any case and still match
+        result = <RouteMatch>[match];
+      } else if (route.routes.isEmpty) {
+        // If it is partial match but no sub-routes, bail.
+        continue;
+      } else {
+        // Otherwise, recurse
+        final String childRestLoc;
+        final String newParentSubLoc;
+        if (match.route is ShellRouteBase) {
+          childRestLoc = remainingLocation;
+          newParentSubLoc = matchedLocation;
+        } else {
+          assert(location.startsWith(match.matchedLocation));
+          assert(remainingLocation.isNotEmpty);
+
+          childRestLoc = location.substring(match.matchedLocation.length +
+              (match.matchedLocation == '/' ? 0 : 1));
+          newParentSubLoc = match.matchedLocation;
+        }
+
+        final List<RouteMatch>? subRouteMatch = _getLocRouteRecursively(
+          location: location,
+          remainingLocation: childRestLoc,
+          matchedLocation: newParentSubLoc,
+          pathParameters: subPathParameters,
+          routes: route.routes,
+        );
+
+        // If there's no sub-route matches, there is no match for this location
+        if (subRouteMatch == null) {
+          continue;
+        }
+        result = <RouteMatch>[match, ...subRouteMatch];
+      }
+      // Should only reach here if there is a match.
+      break;
+    }
+    if (result != null) {
+      pathParameters.addAll(subPathParameters);
+    }
+    return result;
+  }
+
+  /// Processes redirects by returning a new [RouteMatchList] representing the new
+  /// location.
+  FutureOr<RouteMatchList> redirect(
+      BuildContext context, FutureOr<RouteMatchList> prevMatchListFuture,
+      {required List<RouteMatchList> redirectHistory}) {
+    FutureOr<RouteMatchList> processRedirect(RouteMatchList prevMatchList) {
+      final String prevLocation = prevMatchList.uri.toString();
+      FutureOr<RouteMatchList> processTopLevelRedirect(
+          String? topRedirectLocation) {
+        if (topRedirectLocation != null &&
+            topRedirectLocation != prevLocation) {
+          final RouteMatchList newMatch = _getNewMatches(
+            topRedirectLocation,
+            prevMatchList.uri,
+            redirectHistory,
+          );
+          if (newMatch.isError) {
+            return newMatch;
+          }
+          return redirect(
+            context,
+            newMatch,
+            redirectHistory: redirectHistory,
+          );
+        }
+
+        FutureOr<RouteMatchList> processRouteLevelRedirect(
+            String? routeRedirectLocation) {
+          if (routeRedirectLocation != null &&
+              routeRedirectLocation != prevLocation) {
+            final RouteMatchList newMatch = _getNewMatches(
+              routeRedirectLocation,
+              prevMatchList.uri,
+              redirectHistory,
+            );
+
+            if (newMatch.isError) {
+              return newMatch;
+            }
+            return redirect(
+              context,
+              newMatch,
+              redirectHistory: redirectHistory,
+            );
+          }
+          return prevMatchList;
+        }
+
+        final FutureOr<String?> routeLevelRedirectResult =
+            _getRouteLevelRedirect(context, prevMatchList, 0);
+        if (routeLevelRedirectResult is String?) {
+          return processRouteLevelRedirect(routeLevelRedirectResult);
+        }
+        return routeLevelRedirectResult
+            .then<RouteMatchList>(processRouteLevelRedirect);
+      }
+
+      redirectHistory.add(prevMatchList);
+      // Check for top-level redirect
+      final FutureOr<String?> topRedirectResult = topRedirect(
+        context,
+        GoRouterState(
+          this,
+          location: prevLocation,
+          name: null,
+          // No name available at the top level trim the query params off the
+          // sub-location to match route.redirect
+          matchedLocation: prevMatchList.uri.path,
+          queryParameters: prevMatchList.uri.queryParameters,
+          queryParametersAll: prevMatchList.uri.queryParametersAll,
+          extra: prevMatchList.extra,
+          pageKey: const ValueKey<String>('topLevel'),
+        ),
+      );
+
+      if (topRedirectResult is String?) {
+        return processTopLevelRedirect(topRedirectResult);
+      }
+      return topRedirectResult.then<RouteMatchList>(processTopLevelRedirect);
+    }
+
+    if (prevMatchListFuture is RouteMatchList) {
+      return processRedirect(prevMatchListFuture);
+    }
+    return prevMatchListFuture.then<RouteMatchList>(processRedirect);
+  }
+
+  FutureOr<String?> _getRouteLevelRedirect(
+    BuildContext context,
+    RouteMatchList matchList,
+    int currentCheckIndex,
+  ) {
+    if (currentCheckIndex >= matchList.matches.length) {
+      return null;
+    }
+    final RouteMatch match = matchList.matches[currentCheckIndex];
+    FutureOr<String?> processRouteRedirect(String? newLocation) =>
+        newLocation ??
+        _getRouteLevelRedirect(context, matchList, currentCheckIndex + 1);
+    final RouteBase route = match.route;
+    FutureOr<String?> routeRedirectResult;
+    if (route is GoRoute && route.redirect != null) {
+      routeRedirectResult = route.redirect!(
+        context,
+        GoRouterState(
+          this,
+          location: matchList.uri.toString(),
+          matchedLocation: match.matchedLocation,
+          name: route.name,
+          path: route.path,
+          fullPath: matchList.fullPath,
+          extra: matchList.extra,
+          pathParameters: matchList.pathParameters,
+          queryParameters: matchList.uri.queryParameters,
+          queryParametersAll: matchList.uri.queryParametersAll,
+          pageKey: match.pageKey,
+        ),
+      );
+    }
+    if (routeRedirectResult is String?) {
+      return processRouteRedirect(routeRedirectResult);
+    }
+    return routeRedirectResult.then<String?>(processRouteRedirect);
+  }
+
+  RouteMatchList _getNewMatches(
+    String newLocation,
+    Uri previousLocation,
+    List<RouteMatchList> redirectHistory,
+  ) {
+    try {
+      final RouteMatchList newMatch = findMatch(newLocation);
+      _addRedirect(redirectHistory, newMatch, previousLocation);
+      return newMatch;
+    } on RedirectionError catch (e) {
+      log.info('Redirection error: ${e.message}');
+      return _errorRouteMatchList(e.location, e.message);
+    }
+  }
+
+  /// Adds the redirect to [redirects] if it is valid.
+  ///
+  /// Throws if a loop is detected or the redirection limit is reached.
+  void _addRedirect(
+    List<RouteMatchList> redirects,
+    RouteMatchList newMatch,
+    Uri prevLocation,
+  ) {
+    if (redirects.contains(newMatch)) {
+      throw RedirectionError('redirect loop detected',
+          <RouteMatchList>[...redirects, newMatch], prevLocation);
+    }
+    if (redirects.length > redirectLimit) {
+      throw RedirectionError('too many redirects',
+          <RouteMatchList>[...redirects, newMatch], prevLocation);
+    }
+
+    redirects.add(newMatch);
+
+    log.info('redirecting to $newMatch');
+  }
+
   /// Get the location for the provided route.
   ///
   /// Builds the absolute path for the route, by concatenating the paths of the
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 985dce3..2ba3729 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -10,7 +10,6 @@
 import 'builder.dart';
 import 'configuration.dart';
 import 'match.dart';
-import 'matching.dart';
 import 'misc/errors.dart';
 import 'typedefs.dart';
 
@@ -46,25 +45,10 @@
   /// Set to true to disable creating history entries on the web.
   final bool routerNeglect;
 
-  RouteMatchList _matchList = RouteMatchList.empty;
-
   final RouteConfiguration _configuration;
 
-  /// Stores the number of times each route route has been pushed.
-  ///
-  /// This is used to generate a unique key for each route.
-  ///
-  /// For example, it could be equal to:
-  /// ```dart
-  /// {
-  ///   'family': 1,
-  ///   'family/:fid': 2,
-  /// }
-  /// ```
-  final Map<String, int> _pushCounts = <String, int>{};
-
   _NavigatorStateIterator _createNavigatorStateIterator() =>
-      _NavigatorStateIterator(_matchList, navigatorKey.currentState!);
+      _NavigatorStateIterator(currentConfiguration, navigatorKey.currentState!);
 
   @override
   Future<bool> popRoute() async {
@@ -78,45 +62,6 @@
     return false;
   }
 
-  ValueKey<String> _getNewKeyForPath(String path) {
-    // Remap the pageKey to allow any number of the same page on the stack
-    final int count = (_pushCounts[path] ?? -1) + 1;
-    _pushCounts[path] = count;
-    return ValueKey<String>('$path-p$count');
-  }
-
-  Future<T?> _push<T extends Object?>(
-      RouteMatchList matches, ValueKey<String> pageKey) async {
-    final ImperativeRouteMatch<T> newPageKeyMatch = ImperativeRouteMatch<T>(
-      pageKey: pageKey,
-      matches: matches,
-    );
-
-    _matchList = _matchList.push(newPageKeyMatch);
-    return newPageKeyMatch.future;
-  }
-
-  void _remove(RouteMatch match) {
-    _matchList = _matchList.remove(match);
-  }
-
-  /// Pushes the given location onto the page stack.
-  ///
-  /// See also:
-  /// * [pushReplacement] which replaces the top-most page of the page stack and
-  ///   always use a new page key.
-  /// * [replace] which replaces the top-most page of the page stack but treats
-  ///   it as the same page. The page key will be reused. This will preserve the
-  ///   state and not run any page animation.
-  Future<T?> push<T extends Object?>(RouteMatchList matches) async {
-    assert(matches.last.route is! ShellRoute);
-
-    final ValueKey<String> pageKey = _getNewKeyForPath(matches.fullPath);
-    final Future<T?> future = _push(matches, pageKey);
-    notifyListeners();
-    return future;
-  }
-
   /// Returns `true` if the active Navigator can pop.
   bool canPop() {
     final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
@@ -142,7 +87,7 @@
 
   void _debugAssertMatchListNotEmpty() {
     assert(
-      _matchList.isNotEmpty,
+      currentConfiguration.isNotEmpty,
       'You have popped the last page off of the stack,'
       ' there are no pages left to show',
     );
@@ -157,7 +102,7 @@
     if (match is ImperativeRouteMatch) {
       match.complete(result);
     }
-    _remove(match!);
+    currentConfiguration = currentConfiguration.remove(match!);
     notifyListeners();
     assert(() {
       _debugAssertMatchListNotEmpty();
@@ -166,57 +111,19 @@
     return true;
   }
 
-  /// Replaces the top-most page of the page stack with the given one.
-  ///
-  /// The page key of the new page will always be different from the old one.
-  ///
-  /// See also:
-  /// * [push] which pushes the given location onto the page stack.
-  /// * [replace] which replaces the top-most page of the page stack but treats
-  ///   it as the same page. The page key will be reused. This will preserve the
-  ///   state and not run any page animation.
-  void pushReplacement(RouteMatchList matches) {
-    assert(matches.last.route is! ShellRoute);
-    _remove(_matchList.last);
-    push(matches); // [push] will notify the listeners.
-  }
-
-  /// Replaces the top-most page of the page stack with the given one but treats
-  /// it as the same page.
-  ///
-  /// The page key will be reused. This will preserve the state and not run any
-  /// page animation.
-  ///
-  /// See also:
-  /// * [push] which pushes the given location onto the page stack.
-  /// * [pushReplacement] which replaces the top-most page of the page stack but
-  ///   always uses a new page key.
-  void replace(RouteMatchList matches) {
-    assert(matches.last.route is! ShellRoute);
-    final RouteMatch routeMatch = _matchList.last;
-    final ValueKey<String> pageKey = routeMatch.pageKey;
-    _remove(routeMatch);
-    _push(matches, pageKey);
-    notifyListeners();
-  }
-
-  /// For internal use; visible for testing only.
-  @visibleForTesting
-  RouteMatchList get matches => _matchList;
-
   /// For use by the Router architecture as part of the RouterDelegate.
   GlobalKey<NavigatorState> get navigatorKey => _configuration.navigatorKey;
 
   /// For use by the Router architecture as part of the RouterDelegate.
   @override
-  RouteMatchList get currentConfiguration => _matchList;
+  RouteMatchList currentConfiguration = RouteMatchList.empty;
 
   /// For use by the Router architecture as part of the RouterDelegate.
   @override
   Widget build(BuildContext context) {
     return builder.build(
       context,
-      _matchList,
+      currentConfiguration,
       routerNeglect,
     );
   }
@@ -224,8 +131,8 @@
   /// For use by the Router architecture as part of the RouterDelegate.
   @override
   Future<void> setNewRoutePath(RouteMatchList configuration) {
-    _matchList = configuration;
-    assert(_matchList.isNotEmpty);
+    currentConfiguration = configuration;
+    assert(currentConfiguration.isNotEmpty || currentConfiguration.isError);
     notifyListeners();
     // Use [SynchronousFuture] so that the initial url is processed
     // synchronously and remove unwanted initial animations on deep-linking
diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart
index d78e5bc..0f61e74 100644
--- a/packages/go_router/lib/src/information_provider.dart
+++ b/packages/go_router/lib/src/information_provider.dart
@@ -2,79 +2,229 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
+import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/widgets.dart';
 
+import 'match.dart';
+
+// TODO(chunhtai): remove this ignore and migrate the code
+// https://github.com/flutter/flutter/issues/124045.
+// ignore_for_file: deprecated_member_use
+
+/// The type of the navigation.
+///
+/// This enum is used by [RouteInformationState] to denote the navigation
+/// operations.
+enum NavigatingType {
+  /// Push new location on top of the [RouteInformationState.baseRouteMatchList].
+  push,
+
+  /// Push new location and remove top-most [RouteMatch] of the
+  /// [RouteInformationState.baseRouteMatchList].
+  pushReplacement,
+
+  /// Push new location and replace top-most [RouteMatch] of the
+  /// [RouteInformationState.baseRouteMatchList].
+  replace,
+
+  /// Replace the entire [RouteMatchList] with the new location.
+  go,
+}
+
+/// The data class to be stored in [RouteInformation.state] to be used by
+/// [GoRouteInformationPrarser].
+///
+/// This state class is used internally in go_router and will not be send to
+/// the engine.
+class RouteInformationState<T> {
+  /// Creates an InternalRouteInformationState.
+  @visibleForTesting
+  RouteInformationState({
+    this.extra,
+    this.completer,
+    this.baseRouteMatchList,
+    required this.type,
+  }) : assert((type != NavigatingType.go) ==
+            (completer != null && baseRouteMatchList != null));
+
+  /// The extra object used when navigating with [GoRouter].
+  final Object? extra;
+
+  /// The completer that needs to be complete when the newly added route is
+  /// popped off the screen..
+  ///
+  /// This is only null if [type] is [NavigatingType.go].
+  final Completer<T?>? completer;
+
+  /// The base route match list to push on top to.
+  ///
+  /// This is only null if [type] is [NavigatingType.go].
+  final RouteMatchList? baseRouteMatchList;
+
+  /// The type of navigation.
+  final NavigatingType type;
+}
+
 /// The [RouteInformationProvider] created by go_router.
 class GoRouteInformationProvider extends RouteInformationProvider
     with WidgetsBindingObserver, ChangeNotifier {
   /// Creates a [GoRouteInformationProvider].
   GoRouteInformationProvider({
-    required RouteInformation initialRouteInformation,
+    required String initialLocation,
+    required Object? initialExtra,
     Listenable? refreshListenable,
   })  : _refreshListenable = refreshListenable,
-        _value = initialRouteInformation {
+        _value = RouteInformation(
+          location: initialLocation,
+          state: RouteInformationState<void>(
+              extra: initialExtra, type: NavigatingType.go),
+        ),
+        _valueInEngine = _kEmptyRouteInformation {
     _refreshListenable?.addListener(notifyListeners);
   }
 
   final Listenable? _refreshListenable;
 
   static WidgetsBinding get _binding => WidgetsBinding.instance;
+  static const RouteInformation _kEmptyRouteInformation =
+      RouteInformation(location: '');
 
   @override
   void routerReportsNewRouteInformation(RouteInformation routeInformation,
       {RouteInformationReportingType type =
           RouteInformationReportingType.none}) {
-    // Avoid adding a new history entry if the route is the same as before.
-    final bool replace = type == RouteInformationReportingType.neglect ||
-        (type == RouteInformationReportingType.none &&
-            // TODO(chunhtai): remove this ignore and migrate the code
-            // https://github.com/flutter/flutter/issues/124045.
-            // ignore: deprecated_member_use
-            _valueInEngine.location == routeInformation.location);
+    // GoRouteInformationParser should always report encoded route match list
+    // in the state.
+    assert(routeInformation.state != null);
+    final bool replace;
+    switch (type) {
+      case RouteInformationReportingType.none:
+        if (_valueInEngine.location == routeInformation.location &&
+            const DeepCollectionEquality()
+                .equals(_valueInEngine.state, routeInformation.state)) {
+          return;
+        }
+        replace = _valueInEngine == _kEmptyRouteInformation;
+        break;
+      case RouteInformationReportingType.neglect:
+        replace = true;
+        break;
+      case RouteInformationReportingType.navigate:
+        replace = false;
+        break;
+    }
     SystemNavigator.selectMultiEntryHistory();
-    // TODO(chunhtai): report extra to browser through state if possible
-    // See https://github.com/flutter/flutter/issues/108142
     SystemNavigator.routeInformationUpdated(
       // TODO(chunhtai): remove this ignore and migrate the code
       // https://github.com/flutter/flutter/issues/124045.
-      // ignore: deprecated_member_use, unnecessary_null_checks, unnecessary_non_null_assertion
+      // ignore: unnecessary_null_checks, unnecessary_non_null_assertion
       location: routeInformation.location!,
+      state: routeInformation.state,
       replace: replace,
     );
-    _value = routeInformation;
-    _valueInEngine = routeInformation;
+    _value = _valueInEngine = routeInformation;
   }
 
   @override
   RouteInformation get value => _value;
   RouteInformation _value;
 
-  set value(RouteInformation other) {
+  void _setValue(String location, Object state) {
     final bool shouldNotify =
-        // TODO(chunhtai): remove this ignore and migrate the code
-        // https://github.com/flutter/flutter/issues/124045.
-        // ignore: deprecated_member_use
-        _value.location != other.location || _value.state != other.state;
-    _value = other;
+        _value.location != location || _value.state != state;
+    _value = RouteInformation(location: location, state: state);
     if (shouldNotify) {
       notifyListeners();
     }
   }
 
-  RouteInformation _valueInEngine =
-      // TODO(chunhtai): remove this ignore and migrate the code
-      // https://github.com/flutter/flutter/issues/124045.
-      // ignore: deprecated_member_use
-      RouteInformation(location: _binding.platformDispatcher.defaultRouteName);
+  /// Pushes the `location` as a new route on top of `base`.
+  Future<T?> push<T>(String location,
+      {required RouteMatchList base, Object? extra}) {
+    final Completer<T?> completer = Completer<T?>();
+    _setValue(
+      location,
+      RouteInformationState<T>(
+        extra: extra,
+        baseRouteMatchList: base,
+        completer: completer,
+        type: NavigatingType.push,
+      ),
+    );
+    return completer.future;
+  }
+
+  /// Replace the current route matches with the `location`.
+  void go(String location, {Object? extra}) {
+    _setValue(
+      location,
+      RouteInformationState<void>(
+        extra: extra,
+        type: NavigatingType.go,
+      ),
+    );
+  }
+
+  /// Restores the current route matches with the `encodedMatchList`.
+  void restore(String location, {required Object encodedMatchList}) {
+    _setValue(
+      location,
+      encodedMatchList,
+    );
+  }
+
+  /// Removes the top-most route match from `base` and pushes the `location` as a
+  /// new route on top.
+  Future<T?> pushReplacement<T>(String location,
+      {required RouteMatchList base, Object? extra}) {
+    final Completer<T?> completer = Completer<T?>();
+    _setValue(
+      location,
+      RouteInformationState<T>(
+        extra: extra,
+        baseRouteMatchList: base,
+        completer: completer,
+        type: NavigatingType.pushReplacement,
+      ),
+    );
+    return completer.future;
+  }
+
+  /// Replaces the top-most route match from `base` with the `location`.
+  Future<T?> replace<T>(String location,
+      {required RouteMatchList base, Object? extra}) {
+    final Completer<T?> completer = Completer<T?>();
+    _setValue(
+      location,
+      RouteInformationState<T>(
+        extra: extra,
+        baseRouteMatchList: base,
+        completer: completer,
+        type: NavigatingType.replace,
+      ),
+    );
+    return completer.future;
+  }
+
+  RouteInformation _valueInEngine;
 
   void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
     if (_value == routeInformation) {
       return;
     }
-    _value = routeInformation;
-    _valueInEngine = routeInformation;
+    if (routeInformation.state != null) {
+      _value = _valueInEngine = routeInformation;
+    } else {
+      _value = RouteInformation(
+        location: routeInformation.location,
+        state: RouteInformationState<void>(type: NavigatingType.go),
+      );
+      _valueInEngine = _kEmptyRouteInformation;
+    }
     notifyListeners();
   }
 
@@ -113,9 +263,6 @@
   @override
   Future<bool> didPushRoute(String route) {
     assert(hasListeners);
-    // TODO(chunhtai): remove this ignore and migrate the code
-    // https://github.com/flutter/flutter/issues/124045.
-    // ignore: deprecated_member_use
     _platformReportsNewRouteInformation(RouteInformation(location: route));
     return SynchronousFuture<bool>(true);
   }
diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart
index 24aa058..e048331 100644
--- a/packages/go_router/lib/src/match.dart
+++ b/packages/go_router/lib/src/match.dart
@@ -3,12 +3,14 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 
+import 'package:collection/collection.dart';
+import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 
-import 'matching.dart';
+import 'configuration.dart';
 import 'path_utils.dart';
-import 'route.dart';
 
 /// An matched result by matching a [RouteBase] against a location.
 ///
@@ -19,8 +21,6 @@
   const RouteMatch({
     required this.route,
     required this.matchedLocation,
-    required this.extra,
-    required this.error,
     required this.pageKey,
   });
 
@@ -34,14 +34,11 @@
     required String remainingLocation, // e.g. person/p1
     required String matchedLocation, // e.g. /family/f2
     required Map<String, String> pathParameters,
-    required Object? extra,
   }) {
     if (route is ShellRouteBase) {
       return RouteMatch(
         route: route,
         matchedLocation: remainingLocation,
-        extra: extra,
-        error: null,
         pageKey: ValueKey<String>(route.hashCode.toString()),
       );
     } else if (route is GoRoute) {
@@ -62,12 +59,11 @@
       return RouteMatch(
         route: route,
         matchedLocation: newMatchedLocation,
-        extra: extra,
-        error: null,
         pageKey: ValueKey<String>(route.hashCode.toString()),
       );
     }
-    throw MatcherError('Unexpected route type: $route', remainingLocation);
+    assert(false, 'Unexpected route type: $route');
+    return null;
   }
 
   /// The matched route.
@@ -83,12 +79,6 @@
   /// matchedLocation = '/family/f2'
   final String matchedLocation;
 
-  /// An extra object to pass along with the navigation.
-  final Object? extra;
-
-  /// An exception if there was an error during matching.
-  final Exception? error;
-
   /// Value key of type string, to hold a unique reference to a page.
   final ValueKey<String> pageKey;
 
@@ -100,44 +90,49 @@
     return other is RouteMatch &&
         route == other.route &&
         matchedLocation == other.matchedLocation &&
-        extra == other.extra &&
         pageKey == other.pageKey;
   }
 
   @override
-  int get hashCode => Object.hash(route, matchedLocation, extra, pageKey);
+  int get hashCode => Object.hash(route, matchedLocation, pageKey);
 }
 
 /// The route match that represent route pushed through [GoRouter.push].
-class ImperativeRouteMatch<T> extends RouteMatch {
+class ImperativeRouteMatch extends RouteMatch {
   /// Constructor for [ImperativeRouteMatch].
-  ImperativeRouteMatch({
-    required super.pageKey,
-    required this.matches,
-  })  : _completer = Completer<T?>(),
-        super(
-          route: matches.last.route,
-          matchedLocation: matches.last.matchedLocation,
-          extra: matches.last.extra,
-          error: matches.last.error,
+  ImperativeRouteMatch(
+      {required super.pageKey, required this.matches, required this.completer})
+      : super(
+          route: _getsLastRouteFromMatches(matches),
+          matchedLocation: _getsMatchedLocationFromMatches(matches),
         );
+  static RouteBase _getsLastRouteFromMatches(RouteMatchList matchList) {
+    if (matchList.isError) {
+      return GoRoute(
+          path: 'error', builder: (_, __) => throw UnimplementedError());
+    }
+    return matchList.last.route;
+  }
+
+  static String _getsMatchedLocationFromMatches(RouteMatchList matchList) {
+    if (matchList.isError) {
+      return matchList.uri.toString();
+    }
+    return matchList.last.matchedLocation;
+  }
 
   /// The matches that produces this route match.
   final RouteMatchList matches;
 
   /// The completer for the future returned by [GoRouter.push].
-  final Completer<T?> _completer;
+  final Completer<Object?> completer;
 
   /// Called when the corresponding [Route] associated with this route match is
   /// completed.
   void complete([dynamic value]) {
-    _completer.complete(value as T?);
+    completer.complete(value);
   }
 
-  /// The future of the [RouteMatch] completer.
-  /// When the future completes, this will return the value passed to [complete].
-  Future<T?> get future => _completer.future;
-
   // An ImperativeRouteMatch has its own life cycle due the the _completer.
   // comparing _completer between instances would be the same thing as
   // comparing object reference.
@@ -149,3 +144,304 @@
   @override
   int get hashCode => identityHashCode(this);
 }
+
+/// The list of [RouteMatch] objects.
+///
+/// This corresponds to the GoRouter's history.
+@immutable
+class RouteMatchList {
+  /// RouteMatchList constructor.
+  RouteMatchList({
+    required this.matches,
+    required this.uri,
+    this.extra,
+    this.error,
+    required this.pathParameters,
+  }) : fullPath = _generateFullPath(matches);
+
+  /// Constructs an empty matches object.
+  static RouteMatchList empty = RouteMatchList(
+      matches: const <RouteMatch>[],
+      uri: Uri(),
+      pathParameters: const <String, String>{});
+
+  /// The route matches.
+  final List<RouteMatch> matches;
+
+  /// Parameters for the matched route, URI-encoded.
+  ///
+  /// The parameters only reflects [RouteMatch]s that are not
+  /// [ImperativeRouteMatch].
+  final Map<String, String> pathParameters;
+
+  /// The uri of the current match.
+  ///
+  /// This uri only reflects [RouteMatch]s that are not [ImperativeRouteMatch].
+  final Uri uri;
+
+  /// An extra object to pass along with the navigation.
+  final Object? extra;
+
+  /// An exception if there was an error during matching.
+  final Exception? error;
+
+  /// the full path pattern that matches the uri.
+  ///
+  /// For example:
+  ///
+  /// ```dart
+  /// '/family/:fid/person/:pid'
+  /// ```
+  final String fullPath;
+
+  /// Generates the full path (ex: `'/family/:fid/person/:pid'`) of a list of
+  /// [RouteMatch].
+  ///
+  /// This method ignores [ImperativeRouteMatch]s in the `matches`, as they
+  /// don't contribute to the path.
+  ///
+  /// This methods considers that [matches]'s elements verify the go route
+  /// structure given to `GoRouter`. For example, if the routes structure is
+  ///
+  /// ```dart
+  /// GoRoute(
+  ///   path: '/a',
+  ///   routes: [
+  ///     GoRoute(
+  ///       path: 'b',
+  ///       routes: [
+  ///         GoRoute(
+  ///           path: 'c',
+  ///         ),
+  ///       ],
+  ///     ),
+  ///   ],
+  /// ),
+  /// ```
+  ///
+  /// The [matches] must be the in same order of how GoRoutes are matched.
+  ///
+  /// ```dart
+  /// [RouteMatchA(), RouteMatchB(), RouteMatchC()]
+  /// ```
+  static String _generateFullPath(Iterable<RouteMatch> matches) {
+    final StringBuffer buffer = StringBuffer();
+    bool addsSlash = false;
+    for (final RouteMatch match in matches
+        .where((RouteMatch match) => match is! ImperativeRouteMatch)) {
+      final RouteBase route = match.route;
+      if (route is GoRoute) {
+        if (addsSlash) {
+          buffer.write('/');
+        }
+        buffer.write(route.path);
+        addsSlash = addsSlash || route.path != '/';
+      }
+    }
+    return buffer.toString();
+  }
+
+  /// Returns true if there are no matches.
+  bool get isEmpty => matches.isEmpty;
+
+  /// Returns true if there are matches.
+  bool get isNotEmpty => matches.isNotEmpty;
+
+  /// Returns a new instance of RouteMatchList with the input `match` pushed
+  /// onto the current instance.
+  RouteMatchList push(ImperativeRouteMatch match) {
+    // Imperative route match doesn't change the uri and path parameters.
+    return _copyWith(matches: <RouteMatch>[...matches, match]);
+  }
+
+  /// Returns a new instance of RouteMatchList with the input `match` removed
+  /// from the current instance.
+  RouteMatchList remove(RouteMatch match) {
+    final List<RouteMatch> newMatches = matches.toList();
+    final int index = newMatches.indexOf(match);
+    assert(index != -1);
+    newMatches.removeRange(index, newMatches.length);
+
+    // Also pop ShellRoutes when there are no subsequent route matches
+    while (newMatches.isNotEmpty && newMatches.last.route is ShellRouteBase) {
+      newMatches.removeLast();
+    }
+    // Removing ImperativeRouteMatch should not change uri and pathParameters.
+    if (match is ImperativeRouteMatch) {
+      return _copyWith(matches: newMatches);
+    }
+
+    final String fullPath = _generateFullPath(
+        newMatches.where((RouteMatch match) => match is! ImperativeRouteMatch));
+    // Need to remove path parameters that are no longer in the fullPath.
+    final List<String> newParameters = <String>[];
+    patternToRegExp(fullPath, newParameters);
+    final Set<String> validParameters = newParameters.toSet();
+    final Map<String, String> newPathParameters =
+        Map<String, String>.fromEntries(
+      pathParameters.entries.where((MapEntry<String, String> value) =>
+          validParameters.contains(value.key)),
+    );
+    final Uri newUri =
+        uri.replace(path: patternToPath(fullPath, newPathParameters));
+    return _copyWith(
+      matches: newMatches,
+      uri: newUri,
+      pathParameters: newPathParameters,
+    );
+  }
+
+  /// The last matching route.
+  RouteMatch get last => matches.last;
+
+  /// Returns true if the current match intends to display an error screen.
+  bool get isError => error != null;
+
+  /// The routes for each of the matches.
+  List<RouteBase> get routes => matches.map((RouteMatch e) => e.route).toList();
+
+  RouteMatchList _copyWith({
+    List<RouteMatch>? matches,
+    Uri? uri,
+    Map<String, String>? pathParameters,
+  }) {
+    return RouteMatchList(
+        matches: matches ?? this.matches,
+        uri: uri ?? this.uri,
+        extra: extra,
+        error: error,
+        pathParameters: pathParameters ?? this.pathParameters);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is RouteMatchList &&
+        uri == other.uri &&
+        extra == other.extra &&
+        error == other.error &&
+        const ListEquality<RouteMatch>().equals(matches, other.matches) &&
+        const MapEquality<String, String>()
+            .equals(pathParameters, other.pathParameters);
+  }
+
+  @override
+  int get hashCode {
+    return Object.hash(
+      Object.hashAll(matches),
+      uri,
+      extra,
+      error,
+      Object.hashAllUnordered(
+        pathParameters.entries.map<int>((MapEntry<String, String> entry) =>
+            Object.hash(entry.key, entry.value)),
+      ),
+    );
+  }
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)';
+  }
+}
+
+/// Handles encoding and decoding of [RouteMatchList] objects to a format
+/// suitable for using with [StandardMessageCodec].
+///
+/// The primary use of this class is for state restoration.
+class RouteMatchListCodec extends Codec<RouteMatchList, Map<Object?, Object?>> {
+  /// Creates a new [RouteMatchListCodec] object.
+  RouteMatchListCodec(RouteConfiguration configuration)
+      : decoder = _RouteMatchListDecoder(configuration);
+
+  static const String _locationKey = 'location';
+  static const String _extraKey = 'state';
+  static const String _imperativeMatchesKey = 'imperativeMatches';
+  static const String _pageKey = 'pageKey';
+
+  @override
+  final Converter<RouteMatchList, Map<Object?, Object?>> encoder =
+      const _RouteMatchListEncoder();
+
+  @override
+  final Converter<Map<Object?, Object?>, RouteMatchList> decoder;
+}
+
+class _RouteMatchListEncoder
+    extends Converter<RouteMatchList, Map<Object?, Object?>> {
+  const _RouteMatchListEncoder();
+  @override
+  Map<Object?, Object?> convert(RouteMatchList input) {
+    final List<Map<Object?, Object?>> imperativeMatches = input.matches
+        .whereType<ImperativeRouteMatch>()
+        .map((ImperativeRouteMatch e) => _toPrimitives(
+            e.matches.uri.toString(), e.matches.extra,
+            pageKey: e.pageKey.value))
+        .toList();
+
+    return _toPrimitives(input.uri.toString(), input.extra,
+        imperativeMatches: imperativeMatches);
+  }
+
+  static Map<Object?, Object?> _toPrimitives(String location, Object? extra,
+      {List<Map<Object?, Object?>>? imperativeMatches, String? pageKey}) {
+    String? encodedExtra;
+    try {
+      encodedExtra = json.encoder.convert(extra);
+    } on JsonUnsupportedObjectError {/* give up if not serializable */}
+    return <Object?, Object?>{
+      RouteMatchListCodec._locationKey: location,
+      if (encodedExtra != null) RouteMatchListCodec._extraKey: encodedExtra,
+      if (imperativeMatches != null)
+        RouteMatchListCodec._imperativeMatchesKey: imperativeMatches,
+      if (pageKey != null) RouteMatchListCodec._pageKey: pageKey,
+    };
+  }
+}
+
+class _RouteMatchListDecoder
+    extends Converter<Map<Object?, Object?>, RouteMatchList> {
+  _RouteMatchListDecoder(this.configuration);
+
+  final RouteConfiguration configuration;
+
+  @override
+  RouteMatchList convert(Map<Object?, Object?> input) {
+    final String rootLocation =
+        input[RouteMatchListCodec._locationKey]! as String;
+    final String? encodedExtra =
+        input[RouteMatchListCodec._extraKey] as String?;
+    final Object? extra;
+    if (encodedExtra != null) {
+      extra = json.decoder.convert(encodedExtra);
+    } else {
+      extra = null;
+    }
+    RouteMatchList matchList =
+        configuration.findMatch(rootLocation, extra: extra);
+
+    final List<Object?>? imperativeMatches =
+        input[RouteMatchListCodec._imperativeMatchesKey] as List<Object?>?;
+    if (imperativeMatches != null) {
+      for (final Map<Object?, Object?> encodedImperativeMatch
+          in imperativeMatches.whereType<Map<Object?, Object?>>()) {
+        final RouteMatchList imperativeMatchList =
+            convert(encodedImperativeMatch);
+        final ValueKey<String> pageKey = ValueKey<String>(
+            encodedImperativeMatch[RouteMatchListCodec._pageKey]! as String);
+        final ImperativeRouteMatch imperativeMatch = ImperativeRouteMatch(
+          pageKey: pageKey,
+          // TODO(chunhtai): Figure out a way to preserve future.
+          // https://github.com/flutter/flutter/issues/128122.
+          completer: Completer<Object?>(),
+          matches: imperativeMatchList,
+        );
+        matchList = matchList.push(imperativeMatch);
+      }
+    }
+
+    return matchList;
+  }
+}
diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart
deleted file mode 100644
index b809cca..0000000
--- a/packages/go_router/lib/src/matching.dart
+++ /dev/null
@@ -1,479 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'package:collection/collection.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/widgets.dart';
-
-import 'configuration.dart';
-import 'match.dart';
-import 'path_utils.dart';
-
-/// Converts a location into a list of [RouteMatch] objects.
-class RouteMatcher {
-  /// [RouteMatcher] constructor.
-  RouteMatcher(this.configuration);
-
-  /// The route configuration.
-  final RouteConfiguration configuration;
-
-  /// Finds the routes that matched the given URL.
-  RouteMatchList findMatch(String location, {Object? extra}) {
-    final Uri uri = Uri.parse(canonicalUri(location));
-
-    final Map<String, String> pathParameters = <String, String>{};
-    final List<RouteMatch> matches =
-        _getLocRouteMatches(uri, extra, pathParameters);
-    return RouteMatchList(
-        matches: matches, uri: uri, pathParameters: pathParameters);
-  }
-
-  List<RouteMatch> _getLocRouteMatches(
-      Uri uri, Object? extra, Map<String, String> pathParameters) {
-    final List<RouteMatch>? result = _getLocRouteRecursively(
-      location: uri.path,
-      remainingLocation: uri.path,
-      routes: configuration.routes,
-      matchedLocation: '',
-      pathParameters: pathParameters,
-      extra: extra,
-    );
-
-    if (result == null) {
-      throw MatcherError('no routes for location', uri.toString());
-    }
-
-    return result;
-  }
-}
-
-/// The list of [RouteMatch] objects.
-///
-/// This corresponds to the GoRouter's history.
-@immutable
-class RouteMatchList {
-  /// RouteMatchList constructor.
-  RouteMatchList({
-    required this.matches,
-    required this.uri,
-    required this.pathParameters,
-  }) : fullPath = _generateFullPath(matches);
-
-  /// Constructs an empty matches object.
-  static RouteMatchList empty = RouteMatchList(
-      matches: const <RouteMatch>[],
-      uri: Uri(),
-      pathParameters: const <String, String>{});
-
-  /// The route matches.
-  final List<RouteMatch> matches;
-
-  /// Parameters for the matched route, URI-encoded.
-  ///
-  /// The parameters only reflects [RouteMatch]s that are not
-  /// [ImperativeRouteMatch].
-  final Map<String, String> pathParameters;
-
-  /// The uri of the current match.
-  ///
-  /// This uri only reflects [RouteMatch]s that are not [ImperativeRouteMatch].
-  final Uri uri;
-
-  /// the full path pattern that matches the uri.
-  ///
-  /// For example:
-  ///
-  /// ```dart
-  /// '/family/:fid/person/:pid'
-  /// ```
-  final String fullPath;
-
-  /// Generates the full path (ex: `'/family/:fid/person/:pid'`) of a list of
-  /// [RouteMatch].
-  ///
-  /// This method ignores [ImperativeRouteMatch]s in the `matches`, as they
-  /// don't contribute to the path.
-  ///
-  /// This methods considers that [matches]'s elements verify the go route
-  /// structure given to `GoRouter`. For example, if the routes structure is
-  ///
-  /// ```dart
-  /// GoRoute(
-  ///   path: '/a',
-  ///   routes: [
-  ///     GoRoute(
-  ///       path: 'b',
-  ///       routes: [
-  ///         GoRoute(
-  ///           path: 'c',
-  ///         ),
-  ///       ],
-  ///     ),
-  ///   ],
-  /// ),
-  /// ```
-  ///
-  /// The [matches] must be the in same order of how GoRoutes are matched.
-  ///
-  /// ```dart
-  /// [RouteMatchA(), RouteMatchB(), RouteMatchC()]
-  /// ```
-  static String _generateFullPath(Iterable<RouteMatch> matches) {
-    final StringBuffer buffer = StringBuffer();
-    bool addsSlash = false;
-    for (final RouteMatch match in matches
-        .where((RouteMatch match) => match is! ImperativeRouteMatch)) {
-      final RouteBase route = match.route;
-      if (route is GoRoute) {
-        if (addsSlash) {
-          buffer.write('/');
-        }
-        buffer.write(route.path);
-        addsSlash = addsSlash || route.path != '/';
-      }
-    }
-    return buffer.toString();
-  }
-
-  /// Returns true if there are no matches.
-  bool get isEmpty => matches.isEmpty;
-
-  /// Returns true if there are matches.
-  bool get isNotEmpty => matches.isNotEmpty;
-
-  /// Returns a new instance of RouteMatchList with the input `match` pushed
-  /// onto the current instance.
-  RouteMatchList push<T>(ImperativeRouteMatch<T> match) {
-    // Imperative route match doesn't change the uri and path parameters.
-    return _copyWith(matches: <RouteMatch>[...matches, match]);
-  }
-
-  /// Returns a new instance of RouteMatchList with the input `match` removed
-  /// from the current instance.
-  RouteMatchList remove(RouteMatch match) {
-    final List<RouteMatch> newMatches = matches.toList();
-    final int index = newMatches.indexOf(match);
-    assert(index != -1);
-    newMatches.removeRange(index, newMatches.length);
-
-    // Also pop ShellRoutes when there are no subsequent route matches
-    while (newMatches.isNotEmpty && newMatches.last.route is ShellRouteBase) {
-      newMatches.removeLast();
-    }
-    // Removing ImperativeRouteMatch should not change uri and pathParameters.
-    if (match is ImperativeRouteMatch) {
-      return _copyWith(matches: newMatches);
-    }
-
-    final String fullPath = _generateFullPath(
-        newMatches.where((RouteMatch match) => match is! ImperativeRouteMatch));
-    // Need to remove path parameters that are no longer in the fullPath.
-    final List<String> newParameters = <String>[];
-    patternToRegExp(fullPath, newParameters);
-    final Set<String> validParameters = newParameters.toSet();
-    final Map<String, String> newPathParameters =
-        Map<String, String>.fromEntries(
-      pathParameters.entries.where((MapEntry<String, String> value) =>
-          validParameters.contains(value.key)),
-    );
-    final Uri newUri =
-        uri.replace(path: patternToPath(fullPath, newPathParameters));
-    return _copyWith(
-      matches: newMatches,
-      uri: newUri,
-      pathParameters: newPathParameters,
-    );
-  }
-
-  /// An optional object provided by the app during navigation.
-  Object? get extra => matches.isEmpty ? null : matches.last.extra;
-
-  /// The last matching route.
-  RouteMatch get last => matches.last;
-
-  /// Returns true if the current match intends to display an error screen.
-  bool get isError => error != null;
-
-  /// Returns the error that this match intends to display.
-  Exception? get error => matches.firstOrNull?.error;
-
-  /// The routes for each of the matches.
-  List<RouteBase> get routes => matches.map((RouteMatch e) => e.route).toList();
-
-  RouteMatchList _copyWith({
-    List<RouteMatch>? matches,
-    Uri? uri,
-    Map<String, String>? pathParameters,
-  }) {
-    return RouteMatchList(
-        matches: matches ?? this.matches,
-        uri: uri ?? this.uri,
-        pathParameters: pathParameters ?? this.pathParameters);
-  }
-
-  @override
-  bool operator ==(Object other) {
-    if (other.runtimeType != runtimeType) {
-      return false;
-    }
-    return other is RouteMatchList &&
-        const ListEquality<RouteMatch>().equals(matches, other.matches) &&
-        uri == other.uri &&
-        const MapEquality<String, String>()
-            .equals(pathParameters, other.pathParameters);
-  }
-
-  @override
-  int get hashCode {
-    return Object.hash(
-      Object.hashAll(matches),
-      uri,
-      Object.hashAllUnordered(
-        pathParameters.entries.map<int>((MapEntry<String, String> entry) =>
-            Object.hash(entry.key, entry.value)),
-      ),
-    );
-  }
-
-  @override
-  String toString() {
-    return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)';
-  }
-
-  /// Returns a pre-parsed [RouteInformation], containing a reference to this
-  /// match list.
-  RouteInformation toPreParsedRouteInformation() {
-    return RouteInformation(
-      // TODO(tolo): remove this ignore and migrate the code
-      // https://github.com/flutter/flutter/issues/124045.
-      // ignore: deprecated_member_use
-      location: uri.toString(),
-      state: this,
-    );
-  }
-
-  /// Attempts to extract a pre-parsed match list from the provided
-  /// [RouteInformation].
-  static RouteMatchList? fromPreParsedRouteInformation(
-      RouteInformation routeInformation) {
-    if (routeInformation.state is RouteMatchList) {
-      return routeInformation.state! as RouteMatchList;
-    }
-    return null;
-  }
-}
-
-/// Handles encoding and decoding of [RouteMatchList] objects to a format
-/// suitable for using with [StandardMessageCodec].
-///
-/// The primary use of this class is for state restoration.
-class RouteMatchListCodec {
-  /// Creates a new [RouteMatchListCodec] object.
-  RouteMatchListCodec(this._matcher);
-
-  static const String _encodedDataKey = 'go_router/encoded_route_match_list';
-  static const String _locationKey = 'location';
-  static const String _stateKey = 'state';
-  static const String _imperativeMatchesKey = 'imperativeMatches';
-  static const String _pageKey = 'pageKey';
-
-  final RouteMatcher _matcher;
-
-  /// Encodes the provided [RouteMatchList].
-  Object? encodeMatchList(RouteMatchList matchlist) {
-    if (matchlist.isEmpty) {
-      return null;
-    }
-    final List<Map<Object?, Object?>> imperativeMatches = matchlist.matches
-        .whereType<ImperativeRouteMatch<Object?>>()
-        .map((ImperativeRouteMatch<Object?> e) => _toPrimitives(
-            e.matches.uri.toString(), e.extra,
-            pageKey: e.pageKey.value))
-        .toList();
-
-    return <Object?, Object?>{
-      _encodedDataKey: _toPrimitives(
-          matchlist.uri.toString(), matchlist.matches.first.extra,
-          imperativeMatches: imperativeMatches),
-    };
-  }
-
-  static Map<Object?, Object?> _toPrimitives(String location, Object? state,
-      {List<dynamic>? imperativeMatches, String? pageKey}) {
-    return <Object?, Object?>{
-      _locationKey: location,
-      _stateKey: state,
-      if (imperativeMatches != null) _imperativeMatchesKey: imperativeMatches,
-      if (pageKey != null) _pageKey: pageKey,
-    };
-  }
-
-  /// Attempts to decode the provided object into a [RouteMatchList].
-  RouteMatchList? decodeMatchList(Object? object) {
-    if (object is Map && object[_encodedDataKey] is Map) {
-      final Map<Object?, Object?> data =
-          object[_encodedDataKey] as Map<Object?, Object?>;
-      final Object? rootLocation = data[_locationKey];
-      if (rootLocation is! String) {
-        return null;
-      }
-      final RouteMatchList matchList =
-          _matcher.findMatch(rootLocation, extra: data[_stateKey]);
-
-      final List<Object?>? imperativeMatches =
-          data[_imperativeMatchesKey] as List<Object?>?;
-      if (imperativeMatches != null) {
-        for (int i = 0; i < imperativeMatches.length; i++) {
-          final Object? match = imperativeMatches[i];
-          if (match is! Map ||
-              match[_locationKey] is! String ||
-              match[_pageKey] is! String) {
-            continue;
-          }
-          final ValueKey<String> pageKey =
-              ValueKey<String>(match[_pageKey] as String);
-          final RouteMatchList imperativeMatchList = _matcher.findMatch(
-              match[_locationKey] as String,
-              extra: match[_stateKey]);
-          final ImperativeRouteMatch<Object?> imperativeMatch =
-              ImperativeRouteMatch<Object?>(
-            pageKey: pageKey,
-            matches: imperativeMatchList,
-          );
-          matchList.push(imperativeMatch);
-        }
-      }
-
-      return matchList;
-    }
-    return null;
-  }
-}
-
-/// An error that occurred during matching.
-class MatcherError extends Error {
-  /// Constructs a [MatcherError].
-  MatcherError(String message, this.location) : message = '$message: $location';
-
-  /// The error message.
-  final String message;
-
-  /// The location that failed to match.
-  final String location;
-
-  @override
-  String toString() {
-    return message;
-  }
-}
-
-/// Returns the list of `RouteMatch` corresponding to the given `loc`.
-///
-/// For example, for a given `loc` `/a/b/c/d`, this function will return the
-/// list of [RouteBase] `[GoRouteA(), GoRouterB(), GoRouteC(), GoRouterD()]`.
-///
-/// - [location] is the complete URL to match (without the query parameters). For
-///   example, for the URL `/a/b?c=0`, [location] will be `/a/b`.
-/// - [remainingLocation] is the remaining part of the URL to match while [matchedLocation]
-///   is the part of the URL that has already been matched. For examples, for
-///   the URL `/a/b/c/d`, at some point, [remainingLocation] would be `/c/d` and
-///   [matchedLocation] will be `/a/b`.
-/// - [routes] are the possible [RouteBase] to match to [remainingLocation].
-List<RouteMatch>? _getLocRouteRecursively({
-  required String location,
-  required String remainingLocation,
-  required String matchedLocation,
-  required List<RouteBase> routes,
-  required Map<String, String> pathParameters,
-  required Object? extra,
-}) {
-  List<RouteMatch>? result;
-  late Map<String, String> subPathParameters;
-  // find the set of matches at this level of the tree
-  for (final RouteBase route in routes) {
-    subPathParameters = <String, String>{};
-
-    final RouteMatch? match = RouteMatch.match(
-      route: route,
-      remainingLocation: remainingLocation,
-      matchedLocation: matchedLocation,
-      pathParameters: subPathParameters,
-      extra: extra,
-    );
-
-    if (match == null) {
-      continue;
-    }
-
-    if (match.route is GoRoute &&
-        match.matchedLocation.toLowerCase() == location.toLowerCase()) {
-      // If it is a complete match, then return the matched route
-      // NOTE: need a lower case match because matchedLocation is canonicalized to match
-      // the path case whereas the location can be of any case and still match
-      result = <RouteMatch>[match];
-    } else if (route.routes.isEmpty) {
-      // If it is partial match but no sub-routes, bail.
-      continue;
-    } else {
-      // Otherwise, recurse
-      final String childRestLoc;
-      final String newParentSubLoc;
-      if (match.route is ShellRouteBase) {
-        childRestLoc = remainingLocation;
-        newParentSubLoc = matchedLocation;
-      } else {
-        assert(location.startsWith(match.matchedLocation));
-        assert(remainingLocation.isNotEmpty);
-
-        childRestLoc = location.substring(match.matchedLocation.length +
-            (match.matchedLocation == '/' ? 0 : 1));
-        newParentSubLoc = match.matchedLocation;
-      }
-
-      final List<RouteMatch>? subRouteMatch = _getLocRouteRecursively(
-        location: location,
-        remainingLocation: childRestLoc,
-        matchedLocation: newParentSubLoc,
-        routes: route.routes,
-        pathParameters: subPathParameters,
-        extra: extra,
-      );
-
-      // If there's no sub-route matches, there is no match for this location
-      if (subRouteMatch == null) {
-        continue;
-      }
-      result = <RouteMatch>[match, ...subRouteMatch];
-    }
-    // Should only reach here if there is a match.
-    break;
-  }
-  if (result != null) {
-    pathParameters.addAll(subPathParameters);
-  }
-  return result;
-}
-
-/// The match used when there is an error during parsing.
-RouteMatchList errorScreen(Uri uri, String errorMessage) {
-  final Exception error = Exception(errorMessage);
-  return RouteMatchList(
-    matches: <RouteMatch>[
-      RouteMatch(
-        matchedLocation: uri.path,
-        extra: null,
-        error: error,
-        route: GoRoute(
-          path: uri.toString(),
-          pageBuilder: (BuildContext context, GoRouterState state) {
-            throw UnimplementedError();
-          },
-        ),
-        pageKey: const ValueKey<String>('error'),
-      ),
-    ],
-    uri: uri,
-    pathParameters: const <String, String>{},
-  );
-}
diff --git a/packages/go_router/lib/src/misc/errors.dart b/packages/go_router/lib/src/misc/errors.dart
index 7aca0d8..045e2ea 100644
--- a/packages/go_router/lib/src/misc/errors.dart
+++ b/packages/go_router/lib/src/misc/errors.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import '../match.dart';
+
 /// Thrown when [GoRouter] is used incorrectly.
 class GoError extends Error {
   /// Constructs a [GoError]
@@ -13,3 +15,24 @@
   @override
   String toString() => 'GoError: $message';
 }
+
+/// A configuration error detected while processing redirects.
+class RedirectionError extends Error implements UnsupportedError {
+  /// RedirectionError constructor.
+  RedirectionError(this.message, this.matches, this.location);
+
+  /// The matches that were found while processing redirects.
+  final List<RouteMatchList> matches;
+
+  @override
+  final String message;
+
+  /// The location that was originally navigated to, before redirection began.
+  final Uri location;
+
+  @override
+  String toString() => '${super.toString()} ${<String>[
+        ...matches
+            .map((RouteMatchList routeMatches) => routeMatches.uri.toString()),
+      ].join(' => ')}';
+}
diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart
index 78a7ee0..1790241 100644
--- a/packages/go_router/lib/src/parser.dart
+++ b/packages/go_router/lib/src/parser.dart
@@ -3,17 +3,16 @@
 // found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:math';
 
 import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 
+import '../go_router.dart';
 import 'configuration.dart';
 import 'information_provider.dart';
 import 'logging.dart';
 import 'match.dart';
-import 'matching.dart';
-import 'path_utils.dart';
-import 'redirection.dart';
 
 /// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher].
 /// Also performs redirection using [RouteRedirector].
@@ -21,27 +20,14 @@
   /// Creates a [GoRouteInformationParser].
   GoRouteInformationParser({
     required this.configuration,
-    this.debugRequireGoRouteInformationProvider = false,
-  })  : matcher = RouteMatcher(configuration),
-        redirector = redirect;
+  }) : _routeMatchListCodec = RouteMatchListCodec(configuration);
 
-  /// The route configuration for the app.
+  /// The route configuration used for parsing [RouteInformation]s.
   final RouteConfiguration configuration;
 
-  /// The route matcher.
-  final RouteMatcher matcher;
+  final RouteMatchListCodec _routeMatchListCodec;
 
-  /// The route redirector.
-  final RouteRedirector redirector;
-
-  /// A debug property to assert [GoRouteInformationProvider] is in use along
-  /// with this parser.
-  ///
-  /// An assertion error will be thrown if this property set to true and the
-  /// [GoRouteInformationProvider] is not in use.
-  ///
-  /// Defaults to false.
-  final bool debugRequireGoRouteInformationProvider;
+  final Random _random = Random();
 
   /// The future of current route parsing.
   ///
@@ -55,64 +41,42 @@
     RouteInformation routeInformation,
     BuildContext context,
   ) {
+    assert(routeInformation.state != null);
+    final Object state = routeInformation.state!;
+
+    if (state is! RouteInformationState) {
+      // This is a result of browser backward/forward button or state
+      // restoration. In this case, the route match list is already stored in
+      // the state.
+      final RouteMatchList matchList =
+          _routeMatchListCodec.decode(state as Map<Object?, Object?>);
+      return debugParserFuture = _redirect(context, matchList);
+    }
+
     late final RouteMatchList initialMatches;
-    try {
-      final RouteMatchList? preParsedMatchList =
-          RouteMatchList.fromPreParsedRouteInformation(routeInformation);
-      if (preParsedMatchList != null) {
-        initialMatches = preParsedMatchList;
-      } else {
+    initialMatches =
         // TODO(chunhtai): remove this ignore and migrate the code
         // https://github.com/flutter/flutter/issues/124045.
         // ignore: deprecated_member_use, unnecessary_non_null_assertion
-        initialMatches = matcher.findMatch(routeInformation.location!,
-            extra: routeInformation.state);
-      }
-    } on MatcherError {
+        configuration.findMatch(routeInformation.location!, extra: state.extra);
+    if (initialMatches.isError) {
       // TODO(chunhtai): remove this ignore and migrate the code
       // https://github.com/flutter/flutter/issues/124045.
       // ignore: deprecated_member_use
       log.info('No initial matches: ${routeInformation.location}');
-
-      // If there is a matching error for the initial location, we should
-      // still try to process the top-level redirects.
-      initialMatches = RouteMatchList(
-        matches: const <RouteMatch>[],
-        // TODO(chunhtai): remove this ignore and migrate the code
-        // https://github.com/flutter/flutter/issues/124045.
-        // ignore: deprecated_member_use, unnecessary_non_null_assertion
-        uri: Uri.parse(canonicalUri(routeInformation.location!)),
-        pathParameters: const <String, String>{},
-      );
-    }
-    Future<RouteMatchList> processRedirectorResult(RouteMatchList matches) {
-      if (matches.isEmpty) {
-        return SynchronousFuture<RouteMatchList>(errorScreen(
-            // TODO(chunhtai): remove this ignore and migrate the code
-            // https://github.com/flutter/flutter/issues/124045.
-            // ignore: deprecated_member_use, unnecessary_non_null_assertion
-            Uri.parse(routeInformation.location!),
-            // TODO(chunhtai): remove this ignore and migrate the code
-            // https://github.com/flutter/flutter/issues/124045.
-            // ignore: deprecated_member_use, unnecessary_non_null_assertion
-            MatcherError('no routes for location', routeInformation.location!)
-                .toString()));
-      }
-      return SynchronousFuture<RouteMatchList>(matches);
     }
 
-    final FutureOr<RouteMatchList> redirectorResult = redirector(
+    return debugParserFuture = _redirect(
       context,
-      SynchronousFuture<RouteMatchList>(initialMatches),
-      configuration,
-      matcher,
-      extra: routeInformation.state,
-    );
-    if (redirectorResult is RouteMatchList) {
-      return processRedirectorResult(redirectorResult);
-    }
-
-    return debugParserFuture = redirectorResult.then(processRedirectorResult);
+      initialMatches,
+    ).then<RouteMatchList>((RouteMatchList matchList) {
+      return _updateRouteMatchList(
+        matchList,
+        baseRouteMatchList: state.baseRouteMatchList,
+        completer: state.completer,
+        type: state.type,
+      );
+    });
   }
 
   @override
@@ -128,16 +92,70 @@
     if (configuration.isEmpty) {
       return null;
     }
-    if (configuration.matches.last is ImperativeRouteMatch) {
+    if (GoRouter.optionURLReflectsImperativeAPIs &&
+        configuration.matches.last is ImperativeRouteMatch) {
       configuration =
-          (configuration.matches.last as ImperativeRouteMatch<Object?>).matches;
+          (configuration.matches.last as ImperativeRouteMatch).matches;
     }
     return RouteInformation(
       // TODO(chunhtai): remove this ignore and migrate the code
       // https://github.com/flutter/flutter/issues/124045.
       // ignore: deprecated_member_use
       location: configuration.uri.toString(),
-      state: configuration.extra,
+      state: _routeMatchListCodec.encode(configuration),
     );
   }
+
+  Future<RouteMatchList> _redirect(
+      BuildContext context, RouteMatchList routeMatch) {
+    final FutureOr<RouteMatchList> redirectedFuture = configuration
+        .redirect(context, routeMatch, redirectHistory: <RouteMatchList>[]);
+    if (redirectedFuture is RouteMatchList) {
+      return SynchronousFuture<RouteMatchList>(redirectedFuture);
+    }
+    return redirectedFuture;
+  }
+
+  RouteMatchList _updateRouteMatchList(
+    RouteMatchList newMatchList, {
+    required RouteMatchList? baseRouteMatchList,
+    required Completer<Object?>? completer,
+    required NavigatingType type,
+  }) {
+    switch (type) {
+      case NavigatingType.push:
+        return baseRouteMatchList!.push(
+          ImperativeRouteMatch(
+            pageKey: _getUniqueValueKey(),
+            completer: completer!,
+            matches: newMatchList,
+          ),
+        );
+      case NavigatingType.pushReplacement:
+        final RouteMatch routeMatch = baseRouteMatchList!.last;
+        return baseRouteMatchList.remove(routeMatch).push(
+              ImperativeRouteMatch(
+                pageKey: _getUniqueValueKey(),
+                completer: completer!,
+                matches: newMatchList,
+              ),
+            );
+      case NavigatingType.replace:
+        final RouteMatch routeMatch = baseRouteMatchList!.last;
+        return baseRouteMatchList.remove(routeMatch).push(
+              ImperativeRouteMatch(
+                pageKey: routeMatch.pageKey,
+                completer: completer!,
+                matches: newMatchList,
+              ),
+            );
+      case NavigatingType.go:
+        return newMatchList;
+    }
+  }
+
+  ValueKey<String> _getUniqueValueKey() {
+    return ValueKey<String>(String.fromCharCodes(
+        List<int>.generate(32, (_) => _random.nextInt(33) + 89)));
+  }
 }
diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart
deleted file mode 100644
index 3ac4a5b..0000000
--- a/packages/go_router/lib/src/redirection.dart
+++ /dev/null
@@ -1,237 +0,0 @@
-// Copyright 2013 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import 'dart:async';
-
-import 'package:flutter/cupertino.dart';
-
-import 'configuration.dart';
-import 'logging.dart';
-import 'match.dart';
-import 'matching.dart';
-
-/// A GoRouter redirector function.
-typedef RouteRedirector = FutureOr<RouteMatchList> Function(
-    BuildContext, FutureOr<RouteMatchList>, RouteConfiguration, RouteMatcher,
-    {List<RouteMatchList>? redirectHistory, Object? extra});
-
-/// Processes redirects by returning a new [RouteMatchList] representing the new
-/// location.
-FutureOr<RouteMatchList> redirect(
-    BuildContext context,
-    FutureOr<RouteMatchList> prevMatchListFuture,
-    RouteConfiguration configuration,
-    RouteMatcher matcher,
-    {List<RouteMatchList>? redirectHistory,
-    Object? extra}) {
-  FutureOr<RouteMatchList> processRedirect(RouteMatchList prevMatchList) {
-    final String prevLocation = prevMatchList.uri.toString();
-    FutureOr<RouteMatchList> processTopLevelRedirect(
-        String? topRedirectLocation) {
-      if (topRedirectLocation != null && topRedirectLocation != prevLocation) {
-        final RouteMatchList newMatch = _getNewMatches(
-          topRedirectLocation,
-          prevMatchList.uri,
-          configuration,
-          matcher,
-          redirectHistory!,
-        );
-        if (newMatch.isError) {
-          return newMatch;
-        }
-        return redirect(
-          context,
-          newMatch,
-          configuration,
-          matcher,
-          redirectHistory: redirectHistory,
-          extra: extra,
-        );
-      }
-
-      FutureOr<RouteMatchList> processRouteLevelRedirect(
-          String? routeRedirectLocation) {
-        if (routeRedirectLocation != null &&
-            routeRedirectLocation != prevLocation) {
-          final RouteMatchList newMatch = _getNewMatches(
-            routeRedirectLocation,
-            prevMatchList.uri,
-            configuration,
-            matcher,
-            redirectHistory!,
-          );
-
-          if (newMatch.isError) {
-            return newMatch;
-          }
-          return redirect(
-            context,
-            newMatch,
-            configuration,
-            matcher,
-            redirectHistory: redirectHistory,
-            extra: extra,
-          );
-        }
-        return prevMatchList;
-      }
-
-      final FutureOr<String?> routeLevelRedirectResult =
-          _getRouteLevelRedirect(context, configuration, prevMatchList, 0);
-      if (routeLevelRedirectResult is String?) {
-        return processRouteLevelRedirect(routeLevelRedirectResult);
-      }
-      return routeLevelRedirectResult
-          .then<RouteMatchList>(processRouteLevelRedirect);
-    }
-
-    redirectHistory ??= <RouteMatchList>[prevMatchList];
-    // Check for top-level redirect
-    final FutureOr<String?> topRedirectResult = configuration.topRedirect(
-      context,
-      GoRouterState(
-        configuration,
-        location: prevLocation,
-        name: null,
-        // No name available at the top level trim the query params off the
-        // sub-location to match route.redirect
-        matchedLocation: prevMatchList.uri.path,
-        queryParameters: prevMatchList.uri.queryParameters,
-        queryParametersAll: prevMatchList.uri.queryParametersAll,
-        extra: extra,
-        pageKey: const ValueKey<String>('topLevel'),
-      ),
-    );
-
-    if (topRedirectResult is String?) {
-      return processTopLevelRedirect(topRedirectResult);
-    }
-    return topRedirectResult.then<RouteMatchList>(processTopLevelRedirect);
-  }
-
-  if (prevMatchListFuture is RouteMatchList) {
-    return processRedirect(prevMatchListFuture);
-  }
-  return prevMatchListFuture.then<RouteMatchList>(processRedirect);
-}
-
-FutureOr<String?> _getRouteLevelRedirect(
-  BuildContext context,
-  RouteConfiguration configuration,
-  RouteMatchList matchList,
-  int currentCheckIndex,
-) {
-  if (currentCheckIndex >= matchList.matches.length) {
-    return null;
-  }
-  final RouteMatch match = matchList.matches[currentCheckIndex];
-  FutureOr<String?> processRouteRedirect(String? newLocation) =>
-      newLocation ??
-      _getRouteLevelRedirect(
-          context, configuration, matchList, currentCheckIndex + 1);
-  final RouteBase route = match.route;
-  FutureOr<String?> routeRedirectResult;
-  if (route is GoRoute && route.redirect != null) {
-    routeRedirectResult = route.redirect!(
-      context,
-      GoRouterState(
-        configuration,
-        location: matchList.uri.toString(),
-        matchedLocation: match.matchedLocation,
-        name: route.name,
-        path: route.path,
-        fullPath: matchList.fullPath,
-        extra: match.extra,
-        pathParameters: matchList.pathParameters,
-        queryParameters: matchList.uri.queryParameters,
-        queryParametersAll: matchList.uri.queryParametersAll,
-        pageKey: match.pageKey,
-      ),
-    );
-  }
-  if (routeRedirectResult is String?) {
-    return processRouteRedirect(routeRedirectResult);
-  }
-  return routeRedirectResult.then<String?>(processRouteRedirect);
-}
-
-RouteMatchList _getNewMatches(
-  String newLocation,
-  Uri previousLocation,
-  RouteConfiguration configuration,
-  RouteMatcher matcher,
-  List<RouteMatchList> redirectHistory,
-) {
-  try {
-    final RouteMatchList newMatch = matcher.findMatch(newLocation);
-    _addRedirect(redirectHistory, newMatch, previousLocation,
-        configuration.redirectLimit);
-    return newMatch;
-  } on RedirectionError catch (e) {
-    return _handleRedirectionError(e);
-  } on MatcherError catch (e) {
-    return _handleMatcherError(e);
-  }
-}
-
-RouteMatchList _handleMatcherError(MatcherError error) {
-  // The RouteRedirector uses the matcher to find the match, so a match
-  // exception can happen during redirection. For example, the redirector
-  // redirects from `/a` to `/b`, it needs to get the matches for `/b`.
-  log.info('Match error: ${error.message}');
-  final Uri uri = Uri.parse(error.location);
-  return errorScreen(uri, error.message);
-}
-
-RouteMatchList _handleRedirectionError(RedirectionError error) {
-  log.info('Redirection error: ${error.message}');
-  final Uri uri = error.location;
-  return errorScreen(uri, error.message);
-}
-
-/// A configuration error detected while processing redirects.
-class RedirectionError extends Error implements UnsupportedError {
-  /// RedirectionError constructor.
-  RedirectionError(this.message, this.matches, this.location);
-
-  /// The matches that were found while processing redirects.
-  final List<RouteMatchList> matches;
-
-  @override
-  final String message;
-
-  /// The location that was originally navigated to, before redirection began.
-  final Uri location;
-
-  @override
-  String toString() => '${super.toString()} ${<String>[
-        ...matches
-            .map((RouteMatchList routeMatches) => routeMatches.uri.toString()),
-      ].join(' => ')}';
-}
-
-/// Adds the redirect to [redirects] if it is valid.
-void _addRedirect(List<RouteMatchList> redirects, RouteMatchList newMatch,
-    Uri prevLocation, int redirectLimit) {
-  // Verify that the redirect can be parsed and is not already
-  // in the list of redirects
-  assert(() {
-    if (redirects.contains(newMatch)) {
-      throw RedirectionError('redirect loop detected',
-          <RouteMatchList>[...redirects, newMatch], prevLocation);
-    }
-    if (redirects.length > redirectLimit) {
-      throw RedirectionError('too many redirects',
-          <RouteMatchList>[...redirects, newMatch], prevLocation);
-    }
-    return true;
-  }());
-
-  redirects.add(newMatch);
-
-  assert(() {
-    log.info('redirecting to $newMatch');
-    return true;
-  }());
-}
diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart
index 80a885e..8983a78 100644
--- a/packages/go_router/lib/src/route.dart
+++ b/packages/go_router/lib/src/route.dart
@@ -7,8 +7,8 @@
 import 'package:meta/meta.dart';
 
 import '../go_router.dart';
+import 'configuration.dart';
 import 'match.dart';
-import 'matching.dart';
 import 'path_utils.dart';
 import 'typedefs.dart';
 
@@ -928,6 +928,8 @@
   /// current [StatefulShellBranch].
   final ShellRouteContext shellRouteContext;
 
+  final GoRouter _router;
+
   /// The builder for a custom container for shell route Navigators.
   final ShellNavigationContainerBuilder containerBuilder;
 
@@ -936,8 +938,6 @@
   /// Corresponds to the index in the branches field of [StatefulShellRoute].
   final int currentIndex;
 
-  final GoRouter _router;
-
   /// The associated [StatefulShellRoute].
   StatefulShellRoute get route => shellRouteContext.route as StatefulShellRoute;
 
@@ -948,6 +948,8 @@
   /// [StatefulShellRoute]. If the branch has not been visited before, or if
   /// initialLocation is true, this method will navigate to initial location of
   /// the branch (see [StatefulShellBranch.initialLocation]).
+  // TODO(chunhtai): figure out a way to avoid putting navigation API in widget
+  // class.
   void goBranch(int index, {bool initialLocation = false}) {
     final StatefulShellRoute route =
         shellRouteContext.route as StatefulShellRoute;
@@ -977,7 +979,7 @@
       /// Recursively traverses the routes of the provided StackedShellBranch to
       /// find the first GoRoute, from which a full path will be derived.
       final GoRoute route = branch.defaultRoute!;
-      return _router.locationForRoute(route)!;
+      return _router.configuration.locationForRoute(route)!;
     }
   }
 
@@ -1019,7 +1021,6 @@
   StatefulShellRoute get route => widget.route;
 
   GoRouter get _router => widget._router;
-  RouteMatcher get _matcher => _router.routeInformationParser.matcher;
 
   final Map<StatefulShellBranch, _RestorableRouteMatchList> _branchLocations =
       <StatefulShellBranch, _RestorableRouteMatchList>{};
@@ -1040,7 +1041,7 @@
       [bool register = true]) {
     return _branchLocations.putIfAbsent(branch, () {
       final _RestorableRouteMatchList branchLocation =
-          _RestorableRouteMatchList(_matcher);
+          _RestorableRouteMatchList(_router.configuration);
       if (register) {
         registerForRestoration(
             branchLocation, _branchLocationRestorationScopeId(branch));
@@ -1070,6 +1071,7 @@
     if (index > 0) {
       final List<RouteMatch> matches = matchList.matches.sublist(0, index);
       return RouteMatchList(
+        extra: matchList.extra,
         matches: matches,
         uri: Uri.parse(matches.last.matchedLocation),
         pathParameters: matchList.pathParameters,
@@ -1114,15 +1116,10 @@
   /// the branch (see [StatefulShellBranch.initialLocation]).
   void goBranch(int index, {bool initialLocation = false}) {
     assert(index >= 0 && index < route.branches.length);
-    final RouteMatchList? matchlist =
+    final RouteMatchList? matchList =
         initialLocation ? null : _matchListForBranch(index);
-    if (matchlist != null && matchlist.isNotEmpty) {
-      final RouteInformation preParsed =
-          matchlist.toPreParsedRouteInformation();
-      // TODO(tolo): remove this ignore and migrate the code
-      // https://github.com/flutter/flutter/issues/124045.
-      // ignore: deprecated_member_use, unnecessary_non_null_assertion
-      _router.go(preParsed.location!, extra: preParsed.state);
+    if (matchList != null && matchList.isNotEmpty) {
+      _router.restore(matchList);
     } else {
       _router.go(widget._effectiveInitialBranchLocation(index));
     }
@@ -1169,8 +1166,8 @@
 
 /// [RestorableProperty] for enabling state restoration of [RouteMatchList]s.
 class _RestorableRouteMatchList extends RestorableProperty<RouteMatchList> {
-  _RestorableRouteMatchList(RouteMatcher matcher)
-      : _matchListCodec = RouteMatchListCodec(matcher);
+  _RestorableRouteMatchList(RouteConfiguration configuration)
+      : _matchListCodec = RouteMatchListCodec(configuration);
 
   final RouteMatchListCodec _matchListCodec;
 
@@ -1193,13 +1190,15 @@
 
   @override
   RouteMatchList fromPrimitives(Object? data) {
-    return _matchListCodec.decodeMatchList(data) ?? RouteMatchList.empty;
+    return data == null
+        ? RouteMatchList.empty
+        : _matchListCodec.decode(data as Map<Object?, Object?>);
   }
 
   @override
   Object? toPrimitives() {
     if (value.isNotEmpty) {
-      return _matchListCodec.encodeMatchList(value);
+      return _matchListCodec.encode(value);
     }
     return null;
   }
diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart
index 0e2d59b..eac57c2 100644
--- a/packages/go_router/lib/src/router.dart
+++ b/packages/go_router/lib/src/router.dart
@@ -9,7 +9,7 @@
 import 'information_provider.dart';
 import 'logging.dart';
 import 'match.dart';
-import 'matching.dart';
+import 'misc/errors.dart';
 import 'misc/inherited_router.dart';
 import 'parser.dart';
 import 'typedefs.dart';
@@ -69,37 +69,39 @@
         assert(
           initialExtra == null || initialLocation != null,
           'initialLocation must be set in order to use initialExtra',
-        ) {
+        ),
+        assert(_debugCheckPath(routes, true)),
+        assert(
+            _debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
+        assert(_debugCheckParentNavigatorKeys(
+            routes,
+            navigatorKey == null
+                ? <GlobalKey<NavigatorState>>[]
+                : <GlobalKey<NavigatorState>>[navigatorKey])) {
     setLogging(enabled: debugLogDiagnostics);
     WidgetsFlutterBinding.ensureInitialized();
 
     navigatorKey ??= GlobalKey<NavigatorState>();
 
-    _routeConfiguration = RouteConfiguration(
+    configuration = RouteConfiguration(
       routes: routes,
       topRedirect: redirect ?? (_, __) => null,
       redirectLimit: redirectLimit,
       navigatorKey: navigatorKey,
     );
 
-    _routeInformationParser = GoRouteInformationParser(
-      configuration: _routeConfiguration,
-      debugRequireGoRouteInformationProvider: true,
+    routeInformationParser = GoRouteInformationParser(
+      configuration: configuration,
     );
 
-    _routeInformationProvider = GoRouteInformationProvider(
-      initialRouteInformation: RouteInformation(
-        // TODO(chunhtai): remove this ignore and migrate the code
-        // https://github.com/flutter/flutter/issues/124045.
-        // ignore: deprecated_member_use
-        location: _effectiveInitialLocation(initialLocation),
-        state: initialExtra,
-      ),
+    routeInformationProvider = GoRouteInformationProvider(
+      initialLocation: _effectiveInitialLocation(initialLocation),
+      initialExtra: initialExtra,
       refreshListenable: refreshListenable,
     );
 
-    _routerDelegate = GoRouterDelegate(
-      configuration: _routeConfiguration,
+    routerDelegate = GoRouterDelegate(
+      configuration: configuration,
       errorPageBuilder: errorPageBuilder,
       errorBuilder: errorBuilder,
       routerNeglect: routerNeglect,
@@ -112,7 +114,7 @@
       builderWithNav: (BuildContext context, Widget child) =>
           InheritedGoRouter(goRouter: this, child: child),
     );
-    _routerDelegate.addListener(_handleStateMayChange);
+    routerDelegate.addListener(_handleStateMayChange);
 
     assert(() {
       log.info('setting initial location $initialLocation');
@@ -120,10 +122,125 @@
     }());
   }
 
-  late final RouteConfiguration _routeConfiguration;
-  late final GoRouteInformationParser _routeInformationParser;
-  late final GoRouterDelegate _routerDelegate;
-  late final GoRouteInformationProvider _routeInformationProvider;
+  static bool _debugCheckPath(List<RouteBase> routes, bool isTopLevel) {
+    for (final RouteBase route in routes) {
+      late bool subRouteIsTopLevel;
+      if (route is GoRoute) {
+        if (isTopLevel) {
+          assert(route.path.startsWith('/'),
+              'top-level path must start with "/": $route');
+        } else {
+          assert(!route.path.startsWith('/') && !route.path.endsWith('/'),
+              'sub-route path may not start or end with /: $route');
+        }
+        subRouteIsTopLevel = false;
+      } else if (route is ShellRouteBase) {
+        subRouteIsTopLevel = isTopLevel;
+      }
+      _debugCheckPath(route.routes, subRouteIsTopLevel);
+    }
+    return true;
+  }
+
+  // Check that each parentNavigatorKey refers to either a ShellRoute's
+  // navigatorKey or the root navigator key.
+  static bool _debugCheckParentNavigatorKeys(
+      List<RouteBase> routes, List<GlobalKey<NavigatorState>> allowedKeys) {
+    for (final RouteBase route in routes) {
+      if (route is GoRoute) {
+        final GlobalKey<NavigatorState>? parentKey = route.parentNavigatorKey;
+        if (parentKey != null) {
+          // Verify that the root navigator or a ShellRoute ancestor has a
+          // matching navigator key.
+          assert(
+              allowedKeys.contains(parentKey),
+              'parentNavigatorKey $parentKey must refer to'
+              " an ancestor ShellRoute's navigatorKey or GoRouter's"
+              ' navigatorKey');
+
+          _debugCheckParentNavigatorKeys(
+            route.routes,
+            <GlobalKey<NavigatorState>>[
+              // Once a parentNavigatorKey is used, only that navigator key
+              // or keys above it can be used.
+              ...allowedKeys.sublist(0, allowedKeys.indexOf(parentKey) + 1),
+            ],
+          );
+        } else {
+          _debugCheckParentNavigatorKeys(
+            route.routes,
+            <GlobalKey<NavigatorState>>[
+              ...allowedKeys,
+            ],
+          );
+        }
+      } else if (route is ShellRoute) {
+        _debugCheckParentNavigatorKeys(
+          route.routes,
+          <GlobalKey<NavigatorState>>[...allowedKeys..add(route.navigatorKey)],
+        );
+      } else if (route is StatefulShellRoute) {
+        for (final StatefulShellBranch branch in route.branches) {
+          assert(
+              !allowedKeys.contains(branch.navigatorKey),
+              'StatefulShellBranch must not reuse an ancestor navigatorKey '
+              '(${branch.navigatorKey})');
+
+          _debugCheckParentNavigatorKeys(
+            branch.routes,
+            <GlobalKey<NavigatorState>>[
+              ...allowedKeys,
+              branch.navigatorKey,
+            ],
+          );
+        }
+      }
+    }
+    return true;
+  }
+
+  static bool _debugVerifyNoDuplicatePathParameter(
+      List<RouteBase> routes, Map<String, GoRoute> usedPathParams) {
+    for (final RouteBase route in routes) {
+      if (route is! GoRoute) {
+        continue;
+      }
+      for (final String pathParam in route.pathParameters) {
+        if (usedPathParams.containsKey(pathParam)) {
+          final bool sameRoute = usedPathParams[pathParam] == route;
+          throw GoError(
+              "duplicate path parameter, '$pathParam' found in ${sameRoute ? '$route' : '${usedPathParams[pathParam]}, and $route'}");
+        }
+        usedPathParams[pathParam] = route;
+      }
+      _debugVerifyNoDuplicatePathParameter(route.routes, usedPathParams);
+      route.pathParameters.forEach(usedPathParams.remove);
+    }
+    return true;
+  }
+
+  /// Whether the imperative API affects browser URL bar.
+  ///
+  /// The Imperative APIs refer to [push], [pushReplacement], or [Replace].
+  ///
+  /// If this option is set to true. The URL bar reflects the top-most [GoRoute]
+  /// regardless the [RouteBase]s underneath.
+  ///
+  /// If this option is set to false. The URL bar reflects the [RouteBase]s
+  /// in the current state but ignores any [RouteBase]s that are results of
+  /// imperative API calls.
+  ///
+  /// Defaults to false.
+  ///
+  /// This option is for backward compatibility. It is strongly suggested
+  /// against setting this value to true, as the URL of the top-most [GoRoute]
+  /// is not always deeplink-able.
+  ///
+  /// This option only affects web platform.
+  static bool optionURLReflectsImperativeAPIs = false;
+
+  /// The route configuration used in go_router.
+  late final RouteConfiguration configuration;
 
   @override
   final BackButtonDispatcher backButtonDispatcher;
@@ -131,17 +248,15 @@
   /// The router delegate. Provide this to the MaterialApp or CupertinoApp's
   /// `.router()` constructor
   @override
-  GoRouterDelegate get routerDelegate => _routerDelegate;
+  late final GoRouterDelegate routerDelegate;
 
   /// The route information provider used by [GoRouter].
   @override
-  GoRouteInformationProvider get routeInformationProvider =>
-      _routeInformationProvider;
+  late final GoRouteInformationProvider routeInformationProvider;
 
   /// The route information parser used by [GoRouter].
   @override
-  GoRouteInformationParser get routeInformationParser =>
-      _routeInformationParser;
+  late final GoRouteInformationParser routeInformationParser;
 
   /// Gets the current location.
   // TODO(chunhtai): deprecates this once go_router_builder is migrated to
@@ -150,7 +265,7 @@
   String _location = '/';
 
   /// Returns `true` if there is at least two or more route can be pop.
-  bool canPop() => _routerDelegate.canPop();
+  bool canPop() => routerDelegate.canPop();
 
   void _handleStateMayChange() {
     final String newLocation;
@@ -158,12 +273,12 @@
         routerDelegate.currentConfiguration.matches.last
             is ImperativeRouteMatch) {
       newLocation = (routerDelegate.currentConfiguration.matches.last
-              as ImperativeRouteMatch<Object?>)
+              as ImperativeRouteMatch)
           .matches
           .uri
           .toString();
     } else {
-      newLocation = _routerDelegate.currentConfiguration.uri.toString();
+      newLocation = routerDelegate.currentConfiguration.uri.toString();
     }
     if (_location != newLocation) {
       _location = newLocation;
@@ -178,31 +293,26 @@
     Map<String, String> pathParameters = const <String, String>{},
     Map<String, dynamic> queryParameters = const <String, dynamic>{},
   }) =>
-      _routeInformationParser.configuration.namedLocation(
+      configuration.namedLocation(
         name,
         pathParameters: pathParameters,
         queryParameters: queryParameters,
       );
 
-  /// Get the location for the provided route.
-  ///
-  /// Builds the absolute path for the route, by concatenating the paths of the
-  /// route and all its ancestors.
-  String? locationForRoute(RouteBase route) =>
-      _routeInformationParser.configuration.locationForRoute(route);
-
   /// Navigate to a URI location w/ optional query parameters, e.g.
   /// `/family/f2/person/p1?color=blue`
   void go(String location, {Object? extra}) {
-    assert(() {
-      log.info('going to $location');
-      return true;
-    }());
-    _routeInformationProvider.value =
-        // TODO(chunhtai): remove this ignore and migrate the code
-        // https://github.com/flutter/flutter/issues/124045.
-        // ignore: deprecated_member_use
-        RouteInformation(location: location, state: extra);
+    log.info('going to $location');
+    routeInformationProvider.go(location, extra: extra);
+  }
+
+  /// Restore the RouteMatchList
+  void restore(RouteMatchList matchList) {
+    log.info('going to ${matchList.uri}');
+    routeInformationProvider.restore(
+      matchList.uri.toString(),
+      encodedMatchList: RouteMatchListCodec(configuration).encode(matchList),
+    );
   }
 
   /// Navigate to a named route w/ optional parameters, e.g.
@@ -230,22 +340,12 @@
   ///   it as the same page. The page key will be reused. This will preserve the
   ///   state and not run any page animation.
   Future<T?> push<T extends Object?>(String location, {Object? extra}) async {
-    assert(() {
-      log.info('pushing $location');
-      return true;
-    }());
-    final RouteMatchList matches =
-        await _routeInformationParser.parseRouteInformationWithDependencies(
-      // TODO(chunhtai): remove this ignore and migrate the code
-      // https://github.com/flutter/flutter/issues/124045.
-      // ignore: deprecated_member_use
-      RouteInformation(location: location, state: extra),
-      // TODO(chunhtai): avoid accessing the context directly through global key.
-      // https://github.com/flutter/flutter/issues/99112
-      _routerDelegate.navigatorKey.currentContext!,
+    log.info('pushing $location');
+    return routeInformationProvider.push<T>(
+      location,
+      base: routerDelegate.currentConfiguration,
+      extra: extra,
     );
-
-    return _routerDelegate.push<T>(matches);
   }
 
   /// Push a named route onto the page stack w/ optional parameters, e.g.
@@ -271,20 +371,14 @@
   /// * [replace] which replaces the top-most page of the page stack but treats
   ///   it as the same page. The page key will be reused. This will preserve the
   ///   state and not run any page animation.
-  void pushReplacement(String location, {Object? extra}) {
-    routeInformationParser
-        .parseRouteInformationWithDependencies(
-      // TODO(chunhtai): remove this ignore and migrate the code
-      // https://github.com/flutter/flutter/issues/124045.
-      // ignore: deprecated_member_use
-      RouteInformation(location: location, state: extra),
-      // TODO(chunhtai): avoid accessing the context directly through global key.
-      // https://github.com/flutter/flutter/issues/99112
-      _routerDelegate.navigatorKey.currentContext!,
-    )
-        .then<void>((RouteMatchList matchList) {
-      routerDelegate.pushReplacement(matchList);
-    });
+  Future<T?> pushReplacement<T extends Object?>(String location,
+      {Object? extra}) {
+    log.info('pushReplacement $location');
+    return routeInformationProvider.pushReplacement<T>(
+      location,
+      base: routerDelegate.currentConfiguration,
+      extra: extra,
+    );
   }
 
   /// Replaces the top-most page of the page stack with the named route w/
@@ -294,13 +388,13 @@
   /// See also:
   /// * [goNamed] which navigates a named route.
   /// * [pushNamed] which pushes a named route onto the page stack.
-  void pushReplacementNamed(
+  Future<T?> pushReplacementNamed<T extends Object?>(
     String name, {
     Map<String, String> pathParameters = const <String, String>{},
     Map<String, dynamic> queryParameters = const <String, dynamic>{},
     Object? extra,
   }) {
-    pushReplacement(
+    return pushReplacement<T>(
       namedLocation(name,
           pathParameters: pathParameters, queryParameters: queryParameters),
       extra: extra,
@@ -317,20 +411,13 @@
   /// * [push] which pushes the given location onto the page stack.
   /// * [pushReplacement] which replaces the top-most page of the page stack but
   ///   always uses a new page key.
-  void replace(String location, {Object? extra}) {
-    routeInformationParser
-        .parseRouteInformationWithDependencies(
-      // TODO(chunhtai): remove this ignore and migrate the code
-      // https://github.com/flutter/flutter/issues/124045.
-      // ignore: deprecated_member_use
-      RouteInformation(location: location, state: extra),
-      // TODO(chunhtai): avoid accessing the context directly through global key.
-      // https://github.com/flutter/flutter/issues/99112
-      _routerDelegate.navigatorKey.currentContext!,
-    )
-        .then<void>((RouteMatchList matchList) {
-      routerDelegate.replace(matchList);
-    });
+  Future<T?> replace<T>(String location, {Object? extra}) {
+    log.info('replace $location');
+    return routeInformationProvider.replace<T>(
+      location,
+      base: routerDelegate.currentConfiguration,
+      extra: extra,
+    );
   }
 
   /// Replaces the top-most page with the named route and optional parameters,
@@ -344,13 +431,13 @@
   /// * [pushNamed] which pushes the given location onto the page stack.
   /// * [pushReplacementNamed] which replaces the top-most page of the page
   ///   stack but always uses a new page key.
-  void replaceNamed(
+  Future<T?> replaceNamed<T>(
     String name, {
     Map<String, String> pathParameters = const <String, String>{},
     Map<String, dynamic> queryParameters = const <String, dynamic>{},
     Object? extra,
   }) {
-    replace(
+    return replace(
       namedLocation(name,
           pathParameters: pathParameters, queryParameters: queryParameters),
       extra: extra,
@@ -366,7 +453,7 @@
       log.info('popping $location');
       return true;
     }());
-    _routerDelegate.pop<T>(result);
+    routerDelegate.pop<T>(result);
   }
 
   /// Refresh the route.
@@ -375,7 +462,7 @@
       log.info('refreshing $location');
       return true;
     }());
-    _routeInformationProvider.notifyListeners();
+    routeInformationProvider.notifyListeners();
   }
 
   /// Find the current GoRouter in the widget tree.
@@ -395,9 +482,9 @@
 
   @override
   void dispose() {
-    _routeInformationProvider.dispose();
-    _routerDelegate.removeListener(_handleStateMayChange);
-    _routerDelegate.dispose();
+    routeInformationProvider.dispose();
+    routerDelegate.removeListener(_handleStateMayChange);
+    routerDelegate.dispose();
     super.dispose();
   }
 
diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart
index c360cef..9c634ed 100644
--- a/packages/go_router/lib/src/state.dart
+++ b/packages/go_router/lib/src/state.dart
@@ -27,9 +27,6 @@
     this.error,
     required this.pageKey,
   });
-
-  // TODO(johnpryan): remove once namedLocation is removed from go_router.
-  // See https://github.com/flutter/flutter/issues/107729
   final RouteConfiguration _configuration;
 
   /// The full location of the route, e.g. /family/f2/person/p1
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index 7aae5cb..b71ea71 100644
--- a/packages/go_router/pubspec.yaml
+++ b/packages/go_router/pubspec.yaml
@@ -1,7 +1,7 @@
 name: go_router
 description: A declarative router for Flutter based on Navigation 2 supporting
   deep linking, data-driven routes and more
-version: 7.1.1
+version: 8.0.0
 repository: https://github.com/flutter/packages/tree/main/packages/go_router
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
 
diff --git a/packages/go_router/test/builder_test.dart b/packages/go_router/test/builder_test.dart
index ac97cab..9d42699 100644
--- a/packages/go_router/test/builder_test.dart
+++ b/packages/go_router/test/builder_test.dart
@@ -7,7 +7,6 @@
 import 'package:go_router/src/builder.dart';
 import 'package:go_router/src/configuration.dart';
 import 'package:go_router/src/match.dart';
-import 'package:go_router/src/matching.dart';
 import 'package:go_router/src/router.dart';
 
 import 'test_helpers.dart';
@@ -36,8 +35,6 @@
             RouteMatch(
               route: config.routes.first as GoRoute,
               matchedLocation: '/',
-              extra: null,
-              error: null,
               pageKey: const ValueKey<String>('/'),
             ),
           ],
@@ -121,8 +118,6 @@
             RouteMatch(
               route: config.routes.first as GoRoute,
               matchedLocation: '/',
-              extra: null,
-              error: null,
               pageKey: const ValueKey<String>('/'),
             ),
           ],
@@ -176,15 +171,11 @@
             RouteMatch(
               route: config.routes.first,
               matchedLocation: '',
-              extra: null,
-              error: null,
               pageKey: const ValueKey<String>(''),
             ),
             RouteMatch(
               route: config.routes.first.routes.first,
               matchedLocation: '/details',
-              extra: null,
-              error: null,
               pageKey: const ValueKey<String>('/details'),
             ),
           ],
@@ -251,8 +242,6 @@
             RouteMatch(
               route: config.routes.first.routes.first as GoRoute,
               matchedLocation: '/a/details',
-              extra: null,
-              error: null,
               pageKey: const ValueKey<String>('/a/details'),
             ),
           ],
diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart
index 3124702..ac6ff0e 100644
--- a/packages/go_router/test/configuration_test.dart
+++ b/packages/go_router/test/configuration_test.dart
@@ -5,6 +5,7 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/src/configuration.dart';
+import 'package:go_router/src/misc/errors.dart';
 
 import 'test_helpers.dart';
 
@@ -120,7 +121,7 @@
             },
           );
         },
-        throwsAssertionError,
+        throwsA(isA<GoError>()),
       );
     });
 
@@ -318,7 +319,7 @@
             },
           );
         },
-        throwsAssertionError,
+        throwsA(isA<GoError>()),
       );
     });
 
@@ -373,7 +374,7 @@
             },
           );
         },
-        throwsAssertionError,
+        throwsA(isA<GoError>()),
       );
     });
 
@@ -749,7 +750,7 @@
               return null;
             },
           ),
-          throwsAssertionError,
+          throwsA(isA<GoError>()),
         );
       },
     );
@@ -857,7 +858,7 @@
               return null;
             },
           ),
-          throwsAssertionError,
+          throwsA(isA<GoError>()),
         );
       },
     );
diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart
index c25877b..859a4f2 100644
--- a/packages/go_router/test/delegate_test.dart
+++ b/packages/go_router/test/delegate_test.dart
@@ -79,10 +79,13 @@
         ..push('/error');
       await tester.pumpAndSettle();
 
-      final RouteMatch last = goRouter.routerDelegate.matches.matches.last;
+      final RouteMatch last =
+          goRouter.routerDelegate.currentConfiguration.matches.last;
       await goRouter.routerDelegate.popRoute();
-      expect(goRouter.routerDelegate.matches.matches.length, 1);
-      expect(goRouter.routerDelegate.matches.matches.contains(last), false);
+      expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
+      expect(
+          goRouter.routerDelegate.currentConfiguration.matches.contains(last),
+          false);
     });
 
     testWidgets('pops more than matches count should return false',
@@ -100,24 +103,19 @@
       'It should return different pageKey when push is called',
       (WidgetTester tester) async {
         final GoRouter goRouter = await createGoRouter(tester);
-        expect(goRouter.routerDelegate.matches.matches.length, 1);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
 
         goRouter.push('/a');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
-        expect(
-          goRouter.routerDelegate.matches.matches[1].pageKey,
-          const ValueKey<String>('/a-p0'),
-        );
-
         goRouter.push('/a');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 3);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
         expect(
-          goRouter.routerDelegate.matches.matches[2].pageKey,
-          const ValueKey<String>('/a-p1'),
+          goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
+          isNot(equals(
+              goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
         );
       },
     );
@@ -134,10 +132,11 @@
         goRouter.push('/a');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 3);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
         expect(
-          goRouter.routerDelegate.matches.matches[2].pageKey,
-          const Key('/a-p0'),
+          goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
+          isNot(equals(
+              goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
         );
       },
     );
@@ -154,10 +153,11 @@
         goRouter.push('/c/c2');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 3);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
         expect(
-          goRouter.routerDelegate.matches.matches[2].pageKey,
-          const Key('/c/c2-p0'),
+          goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
+          isNot(equals(
+              goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
         );
       },
     );
@@ -174,10 +174,11 @@
         goRouter.push('/c');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 3);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
         expect(
-          goRouter.routerDelegate.matches.matches[2].pageKey,
-          const Key('/c-p1'),
+          goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
+          isNot(equals(
+              goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
         );
       },
     );
@@ -190,7 +191,7 @@
         final GoRouter goRouter = await createGoRouter(tester);
 
         await tester.pumpAndSettle();
-        expect(goRouter.routerDelegate.matches.matches.length, 1);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
         expect(goRouter.routerDelegate.canPop(), false);
       },
     );
@@ -201,7 +202,7 @@
           ..push('/a');
 
         await tester.pumpAndSettle();
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
         expect(goRouter.routerDelegate.canPop(), true);
       },
     );
@@ -227,22 +228,24 @@
       goRouter.push('/page-0');
 
       goRouter.routerDelegate.addListener(expectAsync0(() {}));
-      final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
-      final RouteMatch last = goRouter.routerDelegate.matches.last;
+      final RouteMatch first =
+          goRouter.routerDelegate.currentConfiguration.matches.first;
+      final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
       goRouter.pushReplacement('/page-1');
-      expect(goRouter.routerDelegate.matches.matches.length, 2);
+      expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
       expect(
-        goRouter.routerDelegate.matches.matches.first,
+        goRouter.routerDelegate.currentConfiguration.matches.first,
         first,
         reason: 'The first match should still be in the list of matches',
       );
       expect(
-        goRouter.routerDelegate.matches.last,
+        goRouter.routerDelegate.currentConfiguration.last,
         isNot(last),
         reason: 'The last match should have been removed',
       );
       expect(
-        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
+        (goRouter.routerDelegate.currentConfiguration.last
+                as ImperativeRouteMatch)
             .matches
             .uri
             .toString(),
@@ -255,28 +258,26 @@
       'It should return different pageKey when pushReplacement is called',
       (WidgetTester tester) async {
         final GoRouter goRouter = await createGoRouter(tester);
-        expect(goRouter.routerDelegate.matches.matches.length, 1);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
         expect(
-          goRouter.routerDelegate.matches.matches[0].pageKey,
+          goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
           isNotNull,
         );
 
         goRouter.push('/a');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
-        expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/a-p0'),
-        );
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
+        final ValueKey<String> prev =
+            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
 
         goRouter.pushReplacement('/a');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
         expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/a-p1'),
+          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
+          isNot(equals(prev)),
         );
       },
     );
@@ -309,22 +310,24 @@
         goRouter.pushNamed('page0');
 
         goRouter.routerDelegate.addListener(expectAsync0(() {}));
-        final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
-        final RouteMatch last = goRouter.routerDelegate.matches.last;
+        final RouteMatch first =
+            goRouter.routerDelegate.currentConfiguration.matches.first;
+        final RouteMatch last =
+            goRouter.routerDelegate.currentConfiguration.last;
         goRouter.pushReplacementNamed('page1');
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
         expect(
-          goRouter.routerDelegate.matches.matches.first,
+          goRouter.routerDelegate.currentConfiguration.matches.first,
           first,
           reason: 'The first match should still be in the list of matches',
         );
         expect(
-          goRouter.routerDelegate.matches.last,
+          goRouter.routerDelegate.currentConfiguration.last,
           isNot(last),
           reason: 'The last match should have been removed',
         );
         expect(
-          goRouter.routerDelegate.matches.last,
+          goRouter.routerDelegate.currentConfiguration.last,
           isA<RouteMatch>().having(
             (RouteMatch match) => (match.route as GoRoute).name,
             'match.route.name',
@@ -356,22 +359,24 @@
       goRouter.push('/page-0');
 
       goRouter.routerDelegate.addListener(expectAsync0(() {}));
-      final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
-      final RouteMatch last = goRouter.routerDelegate.matches.last;
+      final RouteMatch first =
+          goRouter.routerDelegate.currentConfiguration.matches.first;
+      final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
       goRouter.replace('/page-1');
-      expect(goRouter.routerDelegate.matches.matches.length, 2);
+      expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
       expect(
-        goRouter.routerDelegate.matches.matches.first,
+        goRouter.routerDelegate.currentConfiguration.matches.first,
         first,
         reason: 'The first match should still be in the list of matches',
       );
       expect(
-        goRouter.routerDelegate.matches.last,
+        goRouter.routerDelegate.currentConfiguration.last,
         isNot(last),
         reason: 'The last match should have been removed',
       );
       expect(
-        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
+        (goRouter.routerDelegate.currentConfiguration.last
+                as ImperativeRouteMatch)
             .matches
             .uri
             .toString(),
@@ -384,28 +389,26 @@
       'It should use the same pageKey when replace is called (with the same path)',
       (WidgetTester tester) async {
         final GoRouter goRouter = await createGoRouter(tester);
-        expect(goRouter.routerDelegate.matches.matches.length, 1);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
         expect(
-          goRouter.routerDelegate.matches.matches[0].pageKey,
+          goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
           isNotNull,
         );
 
         goRouter.push('/a');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
-        expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/a-p0'),
-        );
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
+        final ValueKey<String> prev =
+            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
 
         goRouter.replace('/a');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
         expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/a-p0'),
+          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
+          prev,
         );
       },
     );
@@ -414,28 +417,26 @@
       'It should use the same pageKey when replace is called (with a different path)',
       (WidgetTester tester) async {
         final GoRouter goRouter = await createGoRouter(tester);
-        expect(goRouter.routerDelegate.matches.matches.length, 1);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
         expect(
-          goRouter.routerDelegate.matches.matches[0].pageKey,
+          goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
           isNotNull,
         );
 
         goRouter.push('/a');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
-        expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/a-p0'),
-        );
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
+        final ValueKey<String> prev =
+            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
 
         goRouter.replace('/');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
         expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/a-p0'),
+          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
+          prev,
         );
       },
     );
@@ -479,22 +480,24 @@
       goRouter.pushNamed('page0');
 
       goRouter.routerDelegate.addListener(expectAsync0(() {}));
-      final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
-      final RouteMatch last = goRouter.routerDelegate.matches.last;
+      final RouteMatch first =
+          goRouter.routerDelegate.currentConfiguration.matches.first;
+      final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
       goRouter.replaceNamed('page1');
-      expect(goRouter.routerDelegate.matches.matches.length, 2);
+      expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
       expect(
-        goRouter.routerDelegate.matches.matches.first,
+        goRouter.routerDelegate.currentConfiguration.matches.first,
         first,
         reason: 'The first match should still be in the list of matches',
       );
       expect(
-        goRouter.routerDelegate.matches.last,
+        goRouter.routerDelegate.currentConfiguration.last,
         isNot(last),
         reason: 'The last match should have been removed',
       );
       expect(
-        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
+        (goRouter.routerDelegate.currentConfiguration.last
+                as ImperativeRouteMatch)
             .matches
             .uri
             .toString(),
@@ -507,28 +510,26 @@
       'It should use the same pageKey when replace is called with the same path',
       (WidgetTester tester) async {
         final GoRouter goRouter = await createGoRouter(tester);
-        expect(goRouter.routerDelegate.matches.matches.length, 1);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
         expect(
-          goRouter.routerDelegate.matches.matches.first.pageKey,
+          goRouter.routerDelegate.currentConfiguration.matches.first.pageKey,
           isNotNull,
         );
 
         goRouter.pushNamed('page0');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
-        expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/page-0-p0'),
-        );
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
+        final ValueKey<String> prev =
+            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
 
         goRouter.replaceNamed('page0');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
         expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/page-0-p0'),
+          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
+          prev,
         );
       },
     );
@@ -537,28 +538,26 @@
       'It should use a new pageKey when replace is called with a different path',
       (WidgetTester tester) async {
         final GoRouter goRouter = await createGoRouter(tester);
-        expect(goRouter.routerDelegate.matches.matches.length, 1);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
         expect(
-          goRouter.routerDelegate.matches.matches.first.pageKey,
+          goRouter.routerDelegate.currentConfiguration.matches.first.pageKey,
           isNotNull,
         );
 
         goRouter.pushNamed('page0');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
-        expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/page-0-p0'),
-        );
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
+        final ValueKey<String> prev =
+            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;
 
         goRouter.replaceNamed('home');
         await tester.pumpAndSettle();
 
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
+        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
         expect(
-          goRouter.routerDelegate.matches.matches.last.pageKey,
-          const ValueKey<String>('/page-0-p0'),
+          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
+          prev,
         );
       },
     );
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index cbdc543..b3b4666 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -6,13 +6,13 @@
 
 import 'dart:async';
 
+import 'package:collection/collection.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/go_router.dart';
 import 'package:go_router/src/match.dart';
-import 'package:go_router/src/matching.dart';
 import 'package:logging/logging.dart';
 
 import 'test_helpers.dart';
@@ -46,7 +46,7 @@
       ];
 
       final GoRouter router = await createRouter(routes, tester);
-      final RouteMatchList matches = router.routerDelegate.matches;
+      final RouteMatchList matches = router.routerDelegate.currentConfiguration;
       expect(matches.matches, hasLength(1));
       expect(matches.uri.toString(), '/');
       expect(find.byType(HomeScreen), findsOneWidget);
@@ -61,7 +61,8 @@
 
       final GoRouter router = await createRouter(routes, tester);
       router.go('/');
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect((matches.first.route as GoRoute).name, '1');
       expect(find.byType(DummyScreen), findsOneWidget);
@@ -129,8 +130,9 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/foo');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
+      expect(matches, hasLength(0));
       expect(find.byType(TestErrorScreen), findsOneWidget);
     });
 
@@ -149,7 +151,8 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/login');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect(matches.first.matchedLocation, '/login');
       expect(find.byType(LoginScreen), findsOneWidget);
@@ -178,7 +181,8 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/login');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect(matches.first.matchedLocation, '/login');
       expect(find.byType(LoginScreen), findsOneWidget);
@@ -202,7 +206,8 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/login/');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect(matches.first.matchedLocation, '/login');
       expect(find.byType(LoginScreen), findsOneWidget);
@@ -221,7 +226,8 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/profile/');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect(matches.first.matchedLocation, '/profile/foo');
       expect(find.byType(DummyScreen), findsOneWidget);
@@ -240,7 +246,8 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/profile/?bar=baz');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect(matches.first.matchedLocation, '/profile/foo');
       expect(find.byType(DummyScreen), findsOneWidget);
@@ -352,7 +359,8 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/login');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches.length, 2);
       expect(matches.first.matchedLocation, '/');
       expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
@@ -390,7 +398,8 @@
 
       final GoRouter router = await createRouter(routes, tester);
       {
-        final RouteMatchList matches = router.routerDelegate.matches;
+        final RouteMatchList matches =
+            router.routerDelegate.currentConfiguration;
         expect(matches.matches, hasLength(1));
         expect(matches.uri.toString(), '/');
         expect(find.byType(HomeScreen), findsOneWidget);
@@ -399,7 +408,8 @@
       router.go('/login');
       await tester.pumpAndSettle();
       {
-        final RouteMatchList matches = router.routerDelegate.matches;
+        final RouteMatchList matches =
+            router.routerDelegate.currentConfiguration;
         expect(matches.matches.length, 2);
         expect(matches.matches.first.matchedLocation, '/');
         expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
@@ -410,7 +420,8 @@
       router.go('/family/f2');
       await tester.pumpAndSettle();
       {
-        final RouteMatchList matches = router.routerDelegate.matches;
+        final RouteMatchList matches =
+            router.routerDelegate.currentConfiguration;
         expect(matches.matches.length, 2);
         expect(matches.matches.first.matchedLocation, '/');
         expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
@@ -421,7 +432,8 @@
       router.go('/family/f2/person/p1');
       await tester.pumpAndSettle();
       {
-        final RouteMatchList matches = router.routerDelegate.matches;
+        final RouteMatchList matches =
+            router.routerDelegate.currentConfiguration;
         expect(matches.matches.length, 3);
         expect(matches.matches.first.matchedLocation, '/');
         expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
@@ -469,19 +481,20 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/bar');
       await tester.pumpAndSettle();
-      List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(2));
       expect(find.byType(Page1Screen), findsOneWidget);
 
       router.go('/foo/bar');
       await tester.pumpAndSettle();
-      matches = router.routerDelegate.matches.matches;
+      matches = router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(2));
       expect(find.byType(FamilyScreen), findsOneWidget);
 
       router.go('/foo');
       await tester.pumpAndSettle();
-      matches = router.routerDelegate.matches.matches;
+      matches = router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(2));
       expect(find.byType(Page2Screen), findsOneWidget);
     });
@@ -592,7 +605,8 @@
       const String loc = '/FaMiLy/f2';
       router.go(loc);
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
 
       // NOTE: match the lower case, since location is canonicalized to match the
       // path case whereas the location can be any case; so long as the path
@@ -616,7 +630,8 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/user');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect(find.byType(DummyScreen), findsOneWidget);
     });
@@ -973,6 +988,7 @@
   group('report correct url', () {
     final List<MethodCall> log = <MethodCall>[];
     setUp(() {
+      GoRouter.optionURLReflectsImperativeAPIs = false;
       _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
           .defaultBinaryMessenger
           .setMockMethodCallHandler(SystemChannels.navigation,
@@ -982,12 +998,43 @@
       });
     });
     tearDown(() {
+      GoRouter.optionURLReflectsImperativeAPIs = false;
       _ambiguate(TestDefaultBinaryMessengerBinding.instance)!
           .defaultBinaryMessenger
           .setMockMethodCallHandler(SystemChannels.navigation, null);
       log.clear();
     });
 
+    testWidgets('on push with optionURLReflectImperativeAPIs = true',
+        (WidgetTester tester) async {
+      GoRouter.optionURLReflectsImperativeAPIs = true;
+      final List<GoRoute> routes = <GoRoute>[
+        GoRoute(
+          path: '/',
+          builder: (_, __) => const DummyScreen(),
+        ),
+        GoRoute(
+          path: '/settings',
+          builder: (_, __) => const DummyScreen(),
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester);
+
+      log.clear();
+      router.push('/settings');
+      final RouteMatchListCodec codec =
+          RouteMatchListCodec(router.configuration);
+      await tester.pumpAndSettle();
+      final ImperativeRouteMatch match = router
+          .routerDelegate.currentConfiguration.last as ImperativeRouteMatch;
+      expect(log, <Object>[
+        isMethodCall('selectMultiEntryHistory', arguments: null),
+        IsRouteUpdateCall('/settings', false, codec.encode(match.matches)),
+      ]);
+      GoRouter.optionURLReflectsImperativeAPIs = false;
+    });
+
     testWidgets('on push', (WidgetTester tester) async {
       final List<GoRoute> routes = <GoRoute>[
         GoRoute(
@@ -1004,10 +1051,13 @@
 
       log.clear();
       router.push('/settings');
+      final RouteMatchListCodec codec =
+          RouteMatchListCodec(router.configuration);
       await tester.pumpAndSettle();
       expect(log, <Object>[
         isMethodCall('selectMultiEntryHistory', arguments: null),
-        const IsRouteUpdateCall('/settings', false, null),
+        IsRouteUpdateCall('/', false,
+            codec.encode(router.routerDelegate.currentConfiguration)),
       ]);
     });
 
@@ -1026,13 +1076,15 @@
 
       final GoRouter router =
           await createRouter(routes, tester, initialLocation: '/settings');
-
+      final RouteMatchListCodec codec =
+          RouteMatchListCodec(router.configuration);
       log.clear();
       router.pop();
       await tester.pumpAndSettle();
       expect(log, <Object>[
         isMethodCall('selectMultiEntryHistory', arguments: null),
-        const IsRouteUpdateCall('/', false, null),
+        IsRouteUpdateCall('/', false,
+            codec.encode(router.routerDelegate.currentConfiguration)),
       ]);
     });
 
@@ -1056,14 +1108,16 @@
 
       final GoRouter router = await createRouter(routes, tester,
           initialLocation: '/settings/profile');
-
+      final RouteMatchListCodec codec =
+          RouteMatchListCodec(router.configuration);
       log.clear();
       router.pop();
       router.pop();
       await tester.pumpAndSettle();
       expect(log, <Object>[
         isMethodCall('selectMultiEntryHistory', arguments: null),
-        const IsRouteUpdateCall('/', false, null),
+        IsRouteUpdateCall('/', false,
+            codec.encode(router.routerDelegate.currentConfiguration)),
       ]);
     });
 
@@ -1082,13 +1136,15 @@
 
       final GoRouter router =
           await createRouter(routes, tester, initialLocation: '/settings/123');
-
+      final RouteMatchListCodec codec =
+          RouteMatchListCodec(router.configuration);
       log.clear();
       router.pop();
       await tester.pumpAndSettle();
       expect(log, <Object>[
         isMethodCall('selectMultiEntryHistory', arguments: null),
-        const IsRouteUpdateCall('/', false, null),
+        IsRouteUpdateCall('/', false,
+            codec.encode(router.routerDelegate.currentConfiguration)),
       ]);
     });
 
@@ -1108,13 +1164,15 @@
 
       final GoRouter router =
           await createRouter(routes, tester, initialLocation: '/123/');
-
+      final RouteMatchListCodec codec =
+          RouteMatchListCodec(router.configuration);
       log.clear();
       router.pop();
       await tester.pumpAndSettle();
       expect(log, <Object>[
         isMethodCall('selectMultiEntryHistory', arguments: null),
-        const IsRouteUpdateCall('/', false, null),
+        IsRouteUpdateCall('/', false,
+            codec.encode(router.routerDelegate.currentConfiguration)),
       ]);
     });
 
@@ -1165,12 +1223,15 @@
         ),
       ];
 
-      await createRouter(routes, tester,
+      final GoRouter router = await createRouter(routes, tester,
           initialLocation: '/b/c', navigatorKey: rootNavigatorKey);
+      final RouteMatchListCodec codec =
+          RouteMatchListCodec(router.configuration);
       expect(find.text('Screen C'), findsOneWidget);
       expect(log, <Object>[
         isMethodCall('selectMultiEntryHistory', arguments: null),
-        const IsRouteUpdateCall('/b/c', false, null),
+        IsRouteUpdateCall('/b/c', true,
+            codec.encode(router.routerDelegate.currentConfiguration)),
       ]);
 
       log.clear();
@@ -1180,10 +1241,70 @@
       expect(find.text('Home'), findsOneWidget);
       expect(log, <Object>[
         isMethodCall('selectMultiEntryHistory', arguments: null),
-        const IsRouteUpdateCall('/', false, null),
+        IsRouteUpdateCall('/', false,
+            codec.encode(router.routerDelegate.currentConfiguration)),
       ]);
     });
 
+    testWidgets('can handle route information update from browser',
+        (WidgetTester tester) async {
+      final List<GoRoute> routes = <GoRoute>[
+        GoRoute(
+          path: '/',
+          builder: (_, __) => const DummyScreen(key: ValueKey<String>('home')),
+          routes: <RouteBase>[
+            GoRoute(
+              path: 'settings',
+              builder: (_, GoRouterState state) =>
+                  DummyScreen(key: ValueKey<String>('settings-${state.extra}')),
+            ),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester);
+      expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
+
+      router.push('/settings', extra: 0);
+      await tester.pumpAndSettle();
+      expect(find.byKey(const ValueKey<String>('settings-0')), findsOneWidget);
+
+      log.clear();
+      router.push('/settings', extra: 1);
+      await tester.pumpAndSettle();
+      expect(find.byKey(const ValueKey<String>('settings-1')), findsOneWidget);
+
+      final Map<Object?, Object?> arguments =
+          log.last.arguments as Map<Object?, Object?>;
+      // Stores the state after the last push. This should contain the encoded
+      // RouteMatchList.
+      final Object? state =
+          (log.last.arguments as Map<Object?, Object?>)['state'];
+      final String location =
+          (arguments['location'] ?? arguments['uri']!) as String;
+
+      router.go('/');
+      await tester.pumpAndSettle();
+      expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
+
+      router.routeInformationProvider.didPushRouteInformation(
+          // TODO(chunhtai): remove this ignore and migrate the code
+          // https://github.com/flutter/flutter/issues/124045.
+          // ignore: deprecated_member_use
+          RouteInformation(location: location, state: state));
+      await tester.pumpAndSettle();
+      // Make sure it has all the imperative routes.
+      expect(find.byKey(const ValueKey<String>('settings-1')), findsOneWidget);
+
+      router.pop();
+      await tester.pumpAndSettle();
+      expect(find.byKey(const ValueKey<String>('settings-0')), findsOneWidget);
+
+      router.pop();
+      await tester.pumpAndSettle();
+      expect(find.byKey(const ValueKey<String>('home')), findsOneWidget);
+    });
+
     testWidgets('works correctly with async redirect',
         (WidgetTester tester) async {
       final UniqueKey login = UniqueKey();
@@ -1198,10 +1319,13 @@
         ),
       ];
       final Completer<void> completer = Completer<void>();
-      await createRouter(routes, tester, redirect: (_, __) async {
+      final GoRouter router =
+          await createRouter(routes, tester, redirect: (_, __) async {
         await completer.future;
         return '/login';
       });
+      final RouteMatchListCodec codec =
+          RouteMatchListCodec(router.configuration);
       await tester.pumpAndSettle();
       expect(find.byKey(login), findsNothing);
       expect(tester.takeException(), isNull);
@@ -1214,7 +1338,8 @@
       expect(tester.takeException(), isNull);
       expect(log, <Object>[
         isMethodCall('selectMultiEntryHistory', arguments: null),
-        const IsRouteUpdateCall('/login', false, null),
+        IsRouteUpdateCall('/login', true,
+            codec.encode(router.routerDelegate.currentConfiguration)),
       ]);
     });
   });
@@ -1487,7 +1612,7 @@
       router.go(loc);
       await tester.pumpAndSettle();
 
-      final RouteMatchList matches = router.routerDelegate.matches;
+      final RouteMatchList matches = router.routerDelegate.currentConfiguration;
       expect(find.byType(DummyScreen), findsOneWidget);
       expect(matches.pathParameters['param1'], param1);
     });
@@ -1511,7 +1636,7 @@
           queryParameters: <String, String>{'param1': param1});
       router.go(loc);
       await tester.pumpAndSettle();
-      final RouteMatchList matches = router.routerDelegate.matches;
+      final RouteMatchList matches = router.routerDelegate.currentConfiguration;
       expect(find.byType(DummyScreen), findsOneWidget);
       expect(matches.uri.queryParameters['param1'], param1);
     });
@@ -1766,8 +1891,9 @@
                       ? '/'
                       : null);
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
+      expect(matches, hasLength(0));
       expect(find.byType(TestErrorScreen), findsOneWidget);
       final TestErrorScreen screen =
           tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
@@ -1791,8 +1917,9 @@
         tester,
       );
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
+      expect(matches, hasLength(0));
       expect(find.byType(TestErrorScreen), findsOneWidget);
       final TestErrorScreen screen =
           tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
@@ -1813,8 +1940,9 @@
             state.matchedLocation == '/' ? '/login' : null,
       );
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
+      expect(matches, hasLength(0));
       expect(find.byType(TestErrorScreen), findsOneWidget);
       final TestErrorScreen screen =
           tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
@@ -1834,8 +1962,9 @@
                     : null,
       );
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
+      expect(matches, hasLength(0));
       expect(find.byType(TestErrorScreen), findsOneWidget);
       final TestErrorScreen screen =
           tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
@@ -1895,7 +2024,8 @@
         },
       );
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect(find.byType(LoginScreen), findsOneWidget);
     });
@@ -1924,7 +2054,8 @@
         initialLocation: loc,
       );
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches, hasLength(1));
       expect(find.byType(HomeScreen), findsOneWidget);
     });
@@ -1965,7 +2096,8 @@
         initialLocation: '/family/f2/person/p1',
       );
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
       expect(matches.length, 3);
       expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
       expect(find.byType(FamilyScreen, skipOffstage: false), findsOneWidget);
@@ -1984,14 +2116,56 @@
         redirectLimit: 10,
       );
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
+      expect(matches, hasLength(0));
       expect(find.byType(TestErrorScreen), findsOneWidget);
       final TestErrorScreen screen =
           tester.widget<TestErrorScreen>(find.byType(TestErrorScreen));
       expect(screen.ex, isNotNull);
     });
 
+    testWidgets('can push error page', (WidgetTester tester) async {
+      final GoRouter router = await createRouter(
+        <GoRoute>[
+          GoRoute(path: '/', builder: (_, __) => const Text('/')),
+        ],
+        tester,
+        errorBuilder: (_, GoRouterState state) {
+          return Text(state.location);
+        },
+      );
+
+      expect(find.text('/'), findsOneWidget);
+
+      router.push('/error1');
+      await tester.pumpAndSettle();
+
+      expect(find.text('/'), findsNothing);
+      expect(find.text('/error1'), findsOneWidget);
+
+      router.push('/error2');
+      await tester.pumpAndSettle();
+
+      expect(find.text('/'), findsNothing);
+      expect(find.text('/error1'), findsNothing);
+      expect(find.text('/error2'), findsOneWidget);
+
+      router.pop();
+      await tester.pumpAndSettle();
+
+      expect(find.text('/'), findsNothing);
+      expect(find.text('/error1'), findsOneWidget);
+      expect(find.text('/error2'), findsNothing);
+
+      router.pop();
+      await tester.pumpAndSettle();
+
+      expect(find.text('/'), findsOneWidget);
+      expect(find.text('/error1'), findsNothing);
+      expect(find.text('/error2'), findsNothing);
+    });
+
     testWidgets('extra not null in redirect', (WidgetTester tester) async {
       bool isCallTopRedirect = false;
       bool isCallRouteRedirect = false;
@@ -2231,7 +2405,8 @@
         final String loc = '/family/$fid';
         router.go(loc);
         await tester.pumpAndSettle();
-        final RouteMatchList matches = router.routerDelegate.matches;
+        final RouteMatchList matches =
+            router.routerDelegate.currentConfiguration;
 
         expect(router.location, loc);
         expect(matches.matches, hasLength(1));
@@ -2260,7 +2435,8 @@
         final String loc = '/family?fid=$fid';
         router.go(loc);
         await tester.pumpAndSettle();
-        final RouteMatchList matches = router.routerDelegate.matches;
+        final RouteMatchList matches =
+            router.routerDelegate.currentConfiguration;
 
         expect(router.location, loc);
         expect(matches.matches, hasLength(1));
@@ -2287,7 +2463,7 @@
       router.go(loc);
       await tester.pumpAndSettle();
 
-      final RouteMatchList matches = router.routerDelegate.matches;
+      final RouteMatchList matches = router.routerDelegate.currentConfiguration;
       expect(find.byType(DummyScreen), findsOneWidget);
       expect(matches.pathParameters['param1'], param1);
     });
@@ -2309,7 +2485,7 @@
       router.go('/page1?param1=$param1');
       await tester.pumpAndSettle();
 
-      final RouteMatchList matches = router.routerDelegate.matches;
+      final RouteMatchList matches = router.routerDelegate.currentConfiguration;
       expect(find.byType(DummyScreen), findsOneWidget);
       expect(matches.uri.queryParameters['param1'], param1);
 
@@ -2317,7 +2493,8 @@
       router.go(loc);
       await tester.pumpAndSettle();
 
-      final RouteMatchList matches2 = router.routerDelegate.matches;
+      final RouteMatchList matches2 =
+          router.routerDelegate.currentConfiguration;
       expect(find.byType(DummyScreen), findsOneWidget);
       expect(matches2.uri.queryParameters['param1'], param1);
     });
@@ -2358,7 +2535,7 @@
         tester,
         initialLocation: '/?id=0&id=1',
       );
-      final RouteMatchList matches = router.routerDelegate.matches;
+      final RouteMatchList matches = router.routerDelegate.currentConfiguration;
       expect(matches.matches, hasLength(1));
       expect(matches.fullPath, '/');
       expect(find.byType(HomeScreen), findsOneWidget);
@@ -2381,7 +2558,7 @@
 
       router.go('/0?id=1');
       await tester.pumpAndSettle();
-      final RouteMatchList matches = router.routerDelegate.matches;
+      final RouteMatchList matches = router.routerDelegate.currentConfiguration;
       expect(matches.matches, hasLength(1));
       expect(matches.fullPath, '/:id');
       expect(find.byType(HomeScreen), findsOneWidget);
@@ -2493,13 +2670,13 @@
 
       router.push(loc);
       await tester.pumpAndSettle();
-      final RouteMatchList matches = router.routerDelegate.matches;
+      final RouteMatchList matches = router.routerDelegate.currentConfiguration;
 
       expect(router.location, loc);
       expect(matches.matches, hasLength(2));
       expect(find.byType(PersonScreen), findsOneWidget);
-      final ImperativeRouteMatch<Object?> imperativeRouteMatch =
-          matches.matches.last as ImperativeRouteMatch<Object?>;
+      final ImperativeRouteMatch imperativeRouteMatch =
+          matches.matches.last as ImperativeRouteMatch;
       expect(imperativeRouteMatch.matches.pathParameters['fid'], fid);
       expect(imperativeRouteMatch.matches.pathParameters['pid'], pid);
     });
@@ -2563,7 +2740,7 @@
 
       router.go(loc);
       await tester.pumpAndSettle();
-      RouteMatchList matches = router.routerDelegate.matches;
+      RouteMatchList matches = router.routerDelegate.currentConfiguration;
 
       expect(router.location, loc);
       expect(matches.matches, hasLength(4));
@@ -2580,7 +2757,7 @@
       await tester.pumpAndSettle();
       expect(find.text('Screen A'), findsNothing);
       expect(find.byType(PersonScreen), findsOneWidget);
-      matches = router.routerDelegate.matches;
+      matches = router.routerDelegate.currentConfiguration;
       expect(matches.pathParameters['fid'], fid);
       expect(matches.pathParameters['pid'], pid);
     });
@@ -2623,7 +2800,8 @@
         'q2': <String>['v2', 'v3'],
       });
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final List<RouteMatch> matches =
+          router.routerDelegate.currentConfiguration.matches;
 
       expect(matches, hasLength(1));
       expectLocationWithQueryParams(router.location);
@@ -2673,7 +2851,8 @@
 
     router.go('/page?q1=v1&q2=v2&q2=v3');
     await tester.pumpAndSettle();
-    final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+    final List<RouteMatch> matches =
+        router.routerDelegate.currentConfiguration.matches;
 
     expect(matches, hasLength(1));
     expectLocationWithQueryParams(router.location);
@@ -2722,7 +2901,8 @@
 
     router.go('/page?q1=v1&q2=v2&q2=v3');
     await tester.pumpAndSettle();
-    final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+    final List<RouteMatch> matches =
+        router.routerDelegate.currentConfiguration.matches;
 
     expect(matches, hasLength(1));
     expectLocationWithQueryParams(router.location);
@@ -4681,7 +4861,11 @@
     if (arguments['uri'] != uri && arguments['location'] != uri) {
       return false;
     }
-    return arguments['state'] == state && arguments['replace'] == replace;
+
+    if (!const DeepCollectionEquality().equals(arguments['state'], state)) {
+      return false;
+    }
+    return arguments['replace'] == replace;
   }
 
   @override
diff --git a/packages/go_router/test/information_provider_test.dart b/packages/go_router/test/information_provider_test.dart
index d5e2a0e..d708515 100644
--- a/packages/go_router/test/information_provider_test.dart
+++ b/packages/go_router/test/information_provider_test.dart
@@ -6,31 +6,31 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/src/information_provider.dart';
 
-// TODO(chunhtai): remove this ignore and migrate the code
-// https://github.com/flutter/flutter/issues/124045.
-// ignore: deprecated_member_use
-const RouteInformation initialRoute = RouteInformation(location: '/');
-// TODO(chunhtai): remove this ignore and migrate the code
-// https://github.com/flutter/flutter/issues/124045.
-// ignore: deprecated_member_use
-const RouteInformation newRoute = RouteInformation(location: '/new');
+const String initialRoute = '/';
+const String newRoute = '/new';
 
 void main() {
   group('GoRouteInformationProvider', () {
     testWidgets('notifies its listeners when set by the app',
         (WidgetTester tester) async {
       late final GoRouteInformationProvider provider =
-          GoRouteInformationProvider(initialRouteInformation: initialRoute);
+          GoRouteInformationProvider(
+              initialLocation: initialRoute, initialExtra: null);
       provider.addListener(expectAsync0(() {}));
-      provider.value = newRoute;
+      provider.go(newRoute);
     });
 
     testWidgets('notifies its listeners when set by the platform',
         (WidgetTester tester) async {
       late final GoRouteInformationProvider provider =
-          GoRouteInformationProvider(initialRouteInformation: initialRoute);
+          GoRouteInformationProvider(
+              initialLocation: initialRoute, initialExtra: null);
       provider.addListener(expectAsync0(() {}));
-      provider.didPushRouteInformation(newRoute);
+      // TODO(chunhtai): remove this ignore and migrate the code
+      // https://github.com/flutter/flutter/issues/124045.
+      // ignore_for_file: deprecated_member_use
+      provider
+          .didPushRouteInformation(const RouteInformation(location: newRoute));
     });
   });
 }
diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart
index 712f379..f9e7307 100644
--- a/packages/go_router/test/match_test.dart
+++ b/packages/go_router/test/match_test.dart
@@ -20,7 +20,6 @@
         remainingLocation: '/users/123',
         matchedLocation: '',
         pathParameters: pathParameters,
-        extra: const _Extra('foo'),
       );
       if (match == null) {
         fail('Null match');
@@ -28,8 +27,6 @@
       expect(match.route, route);
       expect(match.matchedLocation, '/users/123');
       expect(pathParameters['userId'], '123');
-      expect(match.extra, const _Extra('foo'));
-      expect(match.error, isNull);
       expect(match.pageKey, isNotNull);
     });
 
@@ -44,7 +41,6 @@
         remainingLocation: 'users/123',
         matchedLocation: '/home',
         pathParameters: pathParameters,
-        extra: const _Extra('foo'),
       );
       if (match == null) {
         fail('Null match');
@@ -52,8 +48,6 @@
       expect(match.route, route);
       expect(match.matchedLocation, '/home/users/123');
       expect(pathParameters['userId'], '123');
-      expect(match.extra, const _Extra('foo'));
-      expect(match.error, isNull);
       expect(match.pageKey, isNotNull);
     });
 
@@ -73,7 +67,6 @@
         remainingLocation: 'users/123',
         matchedLocation: '/home',
         pathParameters: pathParameters,
-        extra: const _Extra('foo'),
       );
       if (match == null) {
         fail('Null match');
@@ -97,7 +90,6 @@
         remainingLocation: 'users/123',
         matchedLocation: '/home',
         pathParameters: pathParameters,
-        extra: const _Extra('foo'),
       );
 
       final RouteMatch? match2 = RouteMatch.match(
@@ -105,7 +97,6 @@
         remainingLocation: 'users/1234',
         matchedLocation: '/home',
         pathParameters: pathParameters,
-        extra: const _Extra('foo1'),
       );
 
       expect(match1!.pageKey, match2!.pageKey);
@@ -122,7 +113,6 @@
         remainingLocation: 'users/123',
         matchedLocation: '/home',
         pathParameters: pathParameters,
-        extra: const _Extra('foo'),
       );
 
       final RouteMatch? match2 = RouteMatch.match(
@@ -130,7 +120,6 @@
         remainingLocation: 'users/1234',
         matchedLocation: '/home',
         pathParameters: pathParameters,
-        extra: const _Extra('foo1'),
       );
 
       expect(match1!.pageKey, match2!.pageKey);
@@ -138,21 +127,6 @@
   });
 }
 
-@immutable
-class _Extra {
-  const _Extra(this.value);
-
-  final String value;
-
-  @override
-  bool operator ==(Object other) {
-    return other is _Extra && other.value == value;
-  }
-
-  @override
-  int get hashCode => value.hashCode;
-}
-
 Widget _builder(BuildContext context, GoRouterState state) =>
     const Placeholder();
 
diff --git a/packages/go_router/test/matching_test.dart b/packages/go_router/test/matching_test.dart
index 8a1d12a..7784b85 100644
--- a/packages/go_router/test/matching_test.dart
+++ b/packages/go_router/test/matching_test.dart
@@ -2,11 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/src/configuration.dart';
 import 'package:go_router/src/match.dart';
-import 'package:go_router/src/matching.dart';
 import 'package:go_router/src/router.dart';
 
 import 'test_helpers.dart';
@@ -25,7 +26,7 @@
     router.go('/page-0');
     await tester.pumpAndSettle();
 
-    final RouteMatchList matches = router.routerDelegate.matches;
+    final RouteMatchList matches = router.routerDelegate.currentConfiguration;
     expect(matches.toString(), contains('/page-0'));
   });
 
@@ -41,7 +42,6 @@
       remainingLocation: '/page-0',
       matchedLocation: '',
       pathParameters: params1,
-      extra: null,
     )!;
 
     final Map<String, String> params2 = <String, String>{};
@@ -50,7 +50,6 @@
       remainingLocation: '/page-0',
       matchedLocation: '',
       pathParameters: params2,
-      extra: null,
     )!;
 
     final RouteMatchList matches1 = RouteMatchList(
@@ -93,16 +92,17 @@
       navigatorKey: GlobalKey<NavigatorState>(),
       topRedirect: (_, __) => null,
     );
-    final RouteMatcher matcher = RouteMatcher(configuration);
-    final RouteMatchListCodec codec = RouteMatchListCodec(matcher);
+    final RouteMatchListCodec codec = RouteMatchListCodec(configuration);
 
-    final RouteMatchList list1 = matcher.findMatch('/a');
-    final RouteMatchList list2 = matcher.findMatch('/b');
-    list1.push(ImperativeRouteMatch<Object?>(
-        pageKey: const ValueKey<String>('/b-p0'), matches: list2));
+    final RouteMatchList list1 = configuration.findMatch('/a');
+    final RouteMatchList list2 = configuration.findMatch('/b');
+    list1.push(ImperativeRouteMatch(
+        pageKey: const ValueKey<String>('/b-p0'),
+        matches: list2,
+        completer: Completer<Object?>()));
 
-    final Object? encoded = codec.encodeMatchList(list1);
-    final RouteMatchList? decoded = codec.decodeMatchList(encoded);
+    final Map<Object?, Object?> encoded = codec.encode(list1);
+    final RouteMatchList decoded = codec.decode(encoded);
 
     expect(decoded, isNotNull);
     expect(decoded, equals(list1));
diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart
index 76a068b..ee4279b 100644
--- a/packages/go_router/test/parser_test.dart
+++ b/packages/go_router/test/parser_test.dart
@@ -6,15 +6,18 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/go_router.dart';
 import 'package:go_router/src/configuration.dart';
+import 'package:go_router/src/information_provider.dart';
 import 'package:go_router/src/match.dart';
-import 'package:go_router/src/matching.dart';
 import 'package:go_router/src/parser.dart';
 
-RouteInformation createRouteInformation(String location, [Object? state]) {
-  // TODO(chunhtai): remove this ignore and migrate the code
-  // https://github.com/flutter/flutter/issues/124045.
-  // ignore: deprecated_member_use
-  return RouteInformation(location: location, state: state);
+RouteInformation createRouteInformation(String location, [Object? extra]) {
+  return RouteInformation(
+      // TODO(chunhtai): remove this ignore and migrate the code
+      // https://github.com/flutter/flutter/issues/124045.
+      // ignore: deprecated_member_use
+      location: location,
+      state:
+          RouteInformationState<void>(type: NavigatingType.go, extra: extra));
 }
 
 void main() {
@@ -64,7 +67,7 @@
     List<RouteMatch> matches = matchesObj.matches;
     expect(matches.length, 1);
     expect(matchesObj.uri.toString(), '/');
-    expect(matches[0].extra, isNull);
+    expect(matchesObj.extra, isNull);
     expect(matches[0].matchedLocation, '/');
     expect(matches[0].route, routes[0]);
 
@@ -74,11 +77,10 @@
     matches = matchesObj.matches;
     expect(matches.length, 2);
     expect(matchesObj.uri.toString(), '/abc?def=ghi');
-    expect(matches[0].extra, extra);
+    expect(matchesObj.extra, extra);
     expect(matches[0].matchedLocation, '/');
     expect(matches[0].route, routes[0]);
 
-    expect(matches[1].extra, extra);
     expect(matches[1].matchedLocation, '/abc');
     expect(matches[1].route, routes[0].routes[0]);
   });
@@ -195,11 +197,10 @@
         await parser.parseRouteInformationWithDependencies(
             createRouteInformation('/def'), context);
     final List<RouteMatch> matches = matchesObj.matches;
-    expect(matches.length, 1);
+    expect(matches.length, 0);
     expect(matchesObj.uri.toString(), '/def');
-    expect(matches[0].extra, isNull);
-    expect(matches[0].matchedLocation, '/def');
-    expect(matches[0].error!.toString(),
+    expect(matchesObj.extra, isNull);
+    expect(matchesObj.error!.toString(),
         'Exception: no routes for location: /def');
   });
 
@@ -267,10 +268,9 @@
     expect(matchesObj.pathParameters.length, 2);
     expect(matchesObj.pathParameters['uid'], '123');
     expect(matchesObj.pathParameters['fid'], '456');
-    expect(matches[0].extra, isNull);
+    expect(matchesObj.extra, isNull);
     expect(matches[0].matchedLocation, '/');
 
-    expect(matches[1].extra, isNull);
     expect(matches[1].matchedLocation, '/123/family/456');
   });
 
@@ -399,8 +399,8 @@
             createRouteInformation('/abd'), context);
     final List<RouteMatch> matches = matchesObj.matches;
 
-    expect(matches, hasLength(1));
-    expect(matches.first.error, isNotNull);
+    expect(matches, hasLength(0));
+    expect(matchesObj.error, isNotNull);
   });
 
   testWidgets('Creates a match for ShellRoute', (WidgetTester tester) async {
@@ -444,6 +444,6 @@
     final List<RouteMatch> matches = matchesObj.matches;
 
     expect(matches, hasLength(2));
-    expect(matches.first.error, isNull);
+    expect(matchesObj.error, isNull);
   });
 }
diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart
index 19f795c..5383838 100644
--- a/packages/go_router/test/test_helpers.dart
+++ b/packages/go_router/test/test_helpers.dart
@@ -301,8 +301,6 @@
   return RouteMatch(
     route: route,
     matchedLocation: location,
-    extra: null,
-    error: null,
     pageKey: ValueKey<String>(location),
   );
 }