[go_router]Fixes GoRouterState.location and  GoRouterState.param to return correct value (#2786)

* Fixes GoRouterState.location and GoRouterState.param to return correct value

* update

* format
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 61a2de2..333e564 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 5.2.0
+
+- Fixes `GoRouterState.location` and `GoRouterState.param` to return correct value.
+- Cleans up `RouteMatch` and `RouteMatchList` API.
+
 ## 5.1.10
 
 - Fixes link of ShellRoute in README.
diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart
index 3afa6b4..42989d7 100644
--- a/packages/go_router/lib/src/builder.dart
+++ b/packages/go_router/lib/src/builder.dart
@@ -5,6 +5,7 @@
 import 'package:flutter/widgets.dart';
 
 import 'configuration.dart';
+import 'delegate.dart';
 import 'logging.dart';
 import 'match.dart';
 import 'matching.dart';
@@ -75,11 +76,7 @@
                 registry: _registry, child: result);
           } on _RouteBuilderError catch (e) {
             return _buildErrorNavigator(
-                context,
-                e,
-                Uri.parse(matchList.location.toString()),
-                pop,
-                configuration.navigatorKey);
+                context, e, matchList.uri, pop, configuration.navigatorKey);
           }
         },
       ),
@@ -124,13 +121,12 @@
     try {
       final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
           <GlobalKey<NavigatorState>, List<Page<Object?>>>{};
-      final Map<String, String> params = <String, String>{};
       _buildRecursive(context, matchList, 0, onPop, routerNeglect, keyToPage,
-          params, navigatorKey, registry);
+          navigatorKey, registry);
       return keyToPage[navigatorKey]!;
     } on _RouteBuilderError catch (e) {
       return <Page<Object?>>[
-        _buildErrorPage(context, e, matchList.location),
+        _buildErrorPage(context, e, matchList.uri),
       ];
     }
   }
@@ -142,7 +138,6 @@
     VoidCallback pop,
     bool routerNeglect,
     Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPages,
-    Map<String, String> params,
     GlobalKey<NavigatorState> navigatorKey,
     Map<Page<Object?>, GoRouterState> registry,
   ) {
@@ -157,11 +152,7 @@
     }
 
     final RouteBase route = match.route;
-    final Map<String, String> newParams = <String, String>{
-      ...params,
-      ...match.decodedParams
-    };
-    final GoRouterState state = buildState(match, newParams);
+    final GoRouterState state = buildState(matchList, match);
     if (route is GoRoute) {
       final Page<Object?> page = _buildPageForRoute(context, state, match);
       registry[page] = state;
@@ -173,7 +164,7 @@
       keyToPages.putIfAbsent(goRouteNavKey, () => <Page<Object?>>[]).add(page);
 
       _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect,
-          keyToPages, newParams, navigatorKey, registry);
+          keyToPages, navigatorKey, registry);
     } else if (route is ShellRoute) {
       // The key for the Navigator that will display this ShellRoute's page.
       final GlobalKey<NavigatorState> parentNavigatorKey = navigatorKey;
@@ -194,7 +185,7 @@
 
       // Build the remaining pages
       _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect,
-          keyToPages, newParams, shellNavigatorKey, registry);
+          keyToPages, shellNavigatorKey, registry);
 
       // Build the Navigator
       final Widget child = _buildNavigator(
@@ -235,25 +226,27 @@
   /// Helper method that builds a [GoRouterState] object for the given [match]
   /// and [params].
   @visibleForTesting
-  GoRouterState buildState(RouteMatch match, Map<String, String> params) {
+  GoRouterState buildState(RouteMatchList matchList, RouteMatch match) {
     final RouteBase route = match.route;
-    String? name = '';
+    String? name;
     String path = '';
     if (route is GoRoute) {
       name = route.name;
       path = route.path;
     }
+    final RouteMatchList effectiveMatchList =
+        match is ImperativeRouteMatch ? match.matches : matchList;
     return GoRouterState(
       configuration,
-      location: match.fullUriString,
+      location: effectiveMatchList.uri.toString(),
       subloc: match.subloc,
       name: name,
       path: path,
-      fullpath: match.fullpath,
-      params: params,
+      fullpath: effectiveMatchList.fullpath,
+      params: effectiveMatchList.pathParameters,
       error: match.error,
-      queryParams: match.queryParams,
-      queryParametersAll: match.queryParametersAll,
+      queryParams: effectiveMatchList.uri.queryParameters,
+      queryParametersAll: effectiveMatchList.uri.queryParametersAll,
       extra: match.extra,
       pageKey: match.pageKey,
     );
@@ -425,6 +418,7 @@
       queryParams: uri.queryParameters,
       queryParametersAll: uri.queryParametersAll,
       error: Exception(error),
+      pageKey: const ValueKey<String>('error'),
     );
 
     // If the error page builder is provided, use that, otherwise, if the error
diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart
index 8f97a7c..b2f5b73 100644
--- a/packages/go_router/lib/src/configuration.dart
+++ b/packages/go_router/lib/src/configuration.dart
@@ -6,9 +6,9 @@
 
 import 'configuration.dart';
 import 'logging.dart';
+import 'misc/errors.dart';
 import 'path_utils.dart';
 import 'typedefs.dart';
-
 export 'route.dart';
 export 'state.dart';
 
@@ -20,74 +20,95 @@
     required this.redirectLimit,
     required this.topRedirect,
     required this.navigatorKey,
-  }) {
+  })  : assert(_debugCheckPath(routes, true)),
+        assert(
+            _debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
+        assert(_debugCheckParentNavigatorKeys(
+            routes, <GlobalKey<NavigatorState>>[navigatorKey])) {
     _cacheNameToPath('', routes);
-
     log.info(_debugKnownRoutes());
+  }
 
-    assert(() {
-      for (final RouteBase route in routes) {
-        if (route is GoRoute && !route.path.startsWith('/')) {
+  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.path}');
-        } else if (route is ShellRoute) {
-          for (final RouteBase route in routes) {
-            if (route is GoRoute) {
-              assert(route.path.startsWith('/'),
-                  'top-level path must start with "/": ${route.path}');
-            }
-          }
+              '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 ShellRoute) {
+        subRouteIsTopLevel = isTopLevel;
       }
+      _debugCheckPath(route.routes, subRouteIsTopLevel);
+    }
+    return true;
+  }
 
-      // Check that each parentNavigatorKey refers to either a ShellRoute's
-      // navigatorKey or the root navigator key.
-      void checkParentNavigatorKeys(
-          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');
+  // 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');
 
-              checkParentNavigatorKeys(
-                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 {
-              checkParentNavigatorKeys(
-                route.routes,
-                <GlobalKey<NavigatorState>>[
-                  ...allowedKeys,
-                ],
-              );
-            }
-          } else if (route is ShellRoute && route.navigatorKey != null) {
-            checkParentNavigatorKeys(
-              route.routes,
-              <GlobalKey<NavigatorState>>[
-                ...allowedKeys..add(route.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 && route.navigatorKey != null) {
+        _debugCheckParentNavigatorKeys(
+          route.routes,
+          <GlobalKey<NavigatorState>>[...allowedKeys..add(route.navigatorKey)],
+        );
       }
+    }
+    return true;
+  }
 
-      checkParentNavigatorKeys(
-          routes, <GlobalKey<NavigatorState>>[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.pathParams) {
+        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.pathParams.forEach(usedPathParams.remove);
+    }
+    return true;
   }
 
   /// The list of top level routes used by [GoRouterDelegate].
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 1077358..2ccbd4c 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -11,7 +11,6 @@
 import 'configuration.dart';
 import 'match.dart';
 import 'matching.dart';
-import 'misc/errors.dart';
 import 'typedefs.dart';
 
 /// GoRouter implementation of [RouterDelegate].
@@ -44,7 +43,7 @@
   /// Set to true to disable creating history entries on the web.
   final bool routerNeglect;
 
-  RouteMatchList _matchList = RouteMatchList.empty();
+  RouteMatchList _matchList = RouteMatchList.empty;
 
   /// Stores the number of times each route route has been pushed.
   ///
@@ -95,26 +94,21 @@
   }
 
   /// Pushes the given location onto the page stack
-  void push(RouteMatch match) {
-    if (match.route is ShellRoute) {
-      throw GoError('ShellRoutes cannot be pushed');
-    }
+  void push(RouteMatchList matches) {
+    assert(matches.last.route is! ShellRoute);
 
     // Remap the pageKey to allow any number of the same page on the stack
-    final String fullPath = match.fullpath;
-    final int count = (_pushCounts[fullPath] ?? 0) + 1;
-    _pushCounts[fullPath] = count;
-    final ValueKey<String> pageKey = ValueKey<String>('$fullPath-p$count');
-    final RouteMatch newPageKeyMatch = RouteMatch(
-      route: match.route,
-      subloc: match.subloc,
-      fullpath: match.fullpath,
-      encodedParams: match.encodedParams,
-      queryParams: match.queryParams,
-      queryParametersAll: match.queryParametersAll,
-      extra: match.extra,
-      error: match.error,
+    final int count = (_pushCounts[matches.fullpath] ?? 0) + 1;
+    _pushCounts[matches.fullpath] = count;
+    final ValueKey<String> pageKey =
+        ValueKey<String>('${matches.fullpath}-p$count');
+    final ImperativeRouteMatch newPageKeyMatch = ImperativeRouteMatch(
+      route: matches.last.route,
+      subloc: matches.last.subloc,
+      extra: matches.last.extra,
+      error: matches.last.error,
       pageKey: pageKey,
+      matches: matches,
     );
 
     _matchList.push(newPageKeyMatch);
@@ -170,9 +164,9 @@
   ///
   /// See also:
   /// * [push] which pushes the given location onto the page stack.
-  void replace(RouteMatch match) {
+  void replace(RouteMatchList matches) {
     _matchList.pop();
-    push(match); // [push] will notify the listeners.
+    push(matches); // [push] will notify the listeners.
   }
 
   /// For internal use; visible for testing only.
@@ -209,3 +203,20 @@
     return SynchronousFuture<void>(null);
   }
 }
+
+/// The route match that represent route pushed through [GoRouter.push].
+// TODO(chunhtai): Removes this once imperative API no longer insert route match.
+class ImperativeRouteMatch extends RouteMatch {
+  /// Constructor for [ImperativeRouteMatch].
+  ImperativeRouteMatch({
+    required super.route,
+    required super.subloc,
+    required super.extra,
+    required super.error,
+    required super.pageKey,
+    required this.matches,
+  });
+
+  /// The matches that produces this route match.
+  final RouteMatchList matches;
+}
diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart
index 7d08492..9d01fa5 100644
--- a/packages/go_router/lib/src/match.dart
+++ b/packages/go_router/lib/src/match.dart
@@ -15,46 +15,25 @@
   RouteMatch({
     required this.route,
     required this.subloc,
-    required this.fullpath,
-    required this.encodedParams,
-    required this.queryParams,
-    required this.queryParametersAll,
     required this.extra,
     required this.error,
-    this.pageKey,
-  })  : fullUriString = _addQueryParams(subloc, queryParametersAll),
-        assert(Uri.parse(subloc).queryParameters.isEmpty),
-        assert(Uri.parse(fullpath).queryParameters.isEmpty),
-        assert(() {
-          for (final MapEntry<String, String> p in encodedParams.entries) {
-            assert(p.value == Uri.encodeComponent(Uri.decodeComponent(p.value)),
-                'encodedParams[${p.key}] is not encoded properly: "${p.value}"');
-          }
-          return true;
-        }());
+    required this.pageKey,
+  });
 
   // ignore: public_member_api_docs
   static RouteMatch? match({
     required RouteBase route,
     required String restLoc, // e.g. person/p1
     required String parentSubloc, // e.g. /family/f2
-    required String fullpath, // e.g. /family/:fid/person/:pid
-    required Map<String, String> queryParams,
-    required Map<String, List<String>> queryParametersAll,
+    required Map<String, String> pathParameters,
     required Object? extra,
   }) {
     if (route is ShellRoute) {
       return RouteMatch(
         route: route,
         subloc: restLoc,
-        fullpath: '',
-        encodedParams: <String, String>{},
-        queryParams: queryParams,
-        queryParametersAll: queryParametersAll,
         extra: extra,
         error: null,
-        // Provide a unique pageKey to ensure that the page for this ShellRoute is
-        // reused.
         pageKey: ValueKey<String>(route.hashCode.toString()),
       );
     } else if (route is GoRoute) {
@@ -66,17 +45,17 @@
       }
 
       final Map<String, String> encodedParams = route.extractPathParams(match);
+      for (final MapEntry<String, String> param in encodedParams.entries) {
+        pathParameters[param.key] = Uri.decodeComponent(param.value);
+      }
       final String pathLoc = patternToPath(route.path, encodedParams);
       final String subloc = concatenatePaths(parentSubloc, pathLoc);
       return RouteMatch(
         route: route,
         subloc: subloc,
-        fullpath: fullpath,
-        encodedParams: encodedParams,
-        queryParams: queryParams,
-        queryParametersAll: queryParametersAll,
         extra: extra,
         error: null,
+        pageKey: ValueKey<String>(route.hashCode.toString()),
       );
     }
     throw MatcherError('Unexpected route type: $route', restLoc);
@@ -88,41 +67,6 @@
   /// The matched location.
   final String subloc; // e.g. /family/f2
 
-  /// The matched template.
-  final String fullpath; // e.g. /family/:fid
-
-  /// Parameters for the matched route, URI-encoded.
-  final Map<String, String> encodedParams;
-
-  /// The URI query split into a map according to the rules specified for FORM
-  /// post in the [HTML 4.01 specification section
-  /// 17.13.4](https://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4
-  /// "HTML 4.01 section 17.13.4").
-  ///
-  /// If a key occurs more than once in the query string, it is mapped to an
-  /// arbitrary choice of possible value.
-  ///
-  /// If the request is `a/b/?q1=v1&q2=v2&q2=v3`, then [queryParameter] will be
-  /// `{q1: 'v1', q2: 'v2'}`.
-  ///
-  /// See also
-  /// * [queryParametersAll] that can provide a map that maps keys to all of
-  ///   their values.
-  final Map<String, String> queryParams;
-
-  /// Returns the URI query split into a map according to the rules specified
-  /// for FORM post in the [HTML 4.01 specification section
-  /// 17.13.4](https://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4
-  /// "HTML 4.01 section 17.13.4").
-  ///
-  /// Keys are mapped to lists of their values. If a key occurs only once, its
-  /// value is a singleton list. If a key occurs with no value, the empty string
-  /// is used as the value for that occurrence.
-  ///
-  /// If the request is `a/b/?q1=v1&q2=v2&q2=v3`, then [queryParameterAll] with
-  /// be `{q1: ['v1'], q2: ['v2', 'v3']}`.
-  final Map<String, List<String>> queryParametersAll;
-
   /// An extra object to pass along with the navigation.
   final Object? extra;
 
@@ -130,29 +74,5 @@
   final Exception? error;
 
   /// Optional value key of type string, to hold a unique reference to a page.
-  final ValueKey<String>? pageKey;
-
-  /// The full uri string
-  final String fullUriString; // e.g. /family/12?query=14
-
-  static String _addQueryParams(
-      String loc, Map<String, dynamic> queryParametersAll) {
-    final Uri uri = Uri.parse(loc);
-    assert(uri.queryParameters.isEmpty);
-    return Uri(
-            path: uri.path,
-            queryParameters:
-                queryParametersAll.isEmpty ? null : queryParametersAll)
-        .toString();
-  }
-
-  /// Parameters for the matched route, URI-decoded.
-  Map<String, String> get decodedParams => <String, String>{
-        for (final MapEntry<String, String> param in encodedParams.entries)
-          param.key: Uri.decodeComponent(param.value)
-      };
-
-  /// For use by the Router architecture as part of the RouteMatch
-  @override
-  String toString() => 'RouteMatch($fullpath, $encodedParams)';
+  final ValueKey<String> pageKey;
 }
diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart
index 36add3a..d9027f4 100644
--- a/packages/go_router/lib/src/matching.dart
+++ b/packages/go_router/lib/src/matching.dart
@@ -18,27 +18,27 @@
 
   /// Finds the routes that matched the given URL.
   RouteMatchList findMatch(String location, {Object? extra}) {
-    final String canonicalLocation = canonicalUri(location);
+    final Uri uri = Uri.parse(canonicalUri(location));
+
+    final Map<String, String> pathParameters = <String, String>{};
     final List<RouteMatch> matches =
-        _getLocRouteMatches(canonicalLocation, extra);
-    return RouteMatchList(matches);
+        _getLocRouteMatches(uri, extra, pathParameters);
+    return RouteMatchList(matches, uri, pathParameters);
   }
 
-  List<RouteMatch> _getLocRouteMatches(String location, Object? extra) {
-    final Uri uri = Uri.parse(location);
-    final List<RouteMatch> result = _getLocRouteRecursively(
+  List<RouteMatch> _getLocRouteMatches(
+      Uri uri, Object? extra, Map<String, String> pathParameters) {
+    final List<RouteMatch>? result = _getLocRouteRecursively(
       loc: uri.path,
       restLoc: uri.path,
       routes: configuration.routes,
-      parentFullpath: '',
       parentSubloc: '',
-      queryParams: uri.queryParameters,
-      queryParametersAll: uri.queryParametersAll,
+      pathParameters: pathParameters,
       extra: extra,
     );
 
-    if (result.isEmpty) {
-      throw MatcherError('no routes for location', location);
+    if (result == null) {
+      throw MatcherError('no routes for location', uri.toString());
     }
 
     return result;
@@ -48,23 +48,48 @@
 /// The list of [RouteMatch] objects.
 class RouteMatchList {
   /// RouteMatchList constructor.
-  RouteMatchList(this._matches);
+  RouteMatchList(List<RouteMatch> matches, this.uri, this.pathParameters)
+      : _matches = matches,
+        fullpath = _generateFullPath(matches);
 
   /// Constructs an empty matches object.
-  factory RouteMatchList.empty() => RouteMatchList(<RouteMatch>[]);
+  static RouteMatchList empty =
+      RouteMatchList(<RouteMatch>[], Uri.parse(''), const <String, String>{});
+
+  static String _generateFullPath(List<RouteMatch> matches) {
+    final StringBuffer buffer = StringBuffer();
+    bool addsSlash = false;
+    for (final RouteMatch match in matches) {
+      final RouteBase route = match.route;
+      if (route is GoRoute) {
+        if (addsSlash) {
+          buffer.write('/');
+        }
+        buffer.write(route.path);
+        addsSlash = addsSlash || route.path != '/';
+      }
+    }
+    return buffer.toString();
+  }
 
   final List<RouteMatch> _matches;
 
+  /// the full path pattern that matches the uri.
+  /// /family/:fid/person/:pid
+  final String fullpath;
+
+  /// Parameters for the matched route, URI-encoded.
+  final Map<String, String> pathParameters;
+
+  /// The uri of the current match.
+  final Uri uri;
+
   /// Returns true if there are no matches.
   bool get isEmpty => _matches.isEmpty;
 
   /// Returns true if there are matches.
   bool get isNotEmpty => _matches.isNotEmpty;
 
-  /// The original URL that was matched.
-  Uri get location =>
-      _matches.isEmpty ? Uri() : Uri.parse(_matches.last.fullUriString);
-
   /// Pushes a match onto the list of matches.
   void push(RouteMatch match) {
     _matches.add(match);
@@ -113,38 +138,25 @@
   }
 }
 
-List<RouteMatch> _getLocRouteRecursively({
+List<RouteMatch>? _getLocRouteRecursively({
   required String loc,
   required String restLoc,
   required String parentSubloc,
   required List<RouteBase> routes,
-  required String parentFullpath,
-  required Map<String, String> queryParams,
-  required Map<String, List<String>> queryParametersAll,
+  required Map<String, String> pathParameters,
   required Object? extra,
 }) {
-  bool debugGatherAllMatches = false;
-  assert(() {
-    debugGatherAllMatches = true;
-    return true;
-  }());
-  final List<List<RouteMatch>> result = <List<RouteMatch>>[];
+  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) {
-    late final String fullpath;
-    if (route is GoRoute) {
-      fullpath = concatenatePaths(parentFullpath, route.path);
-    } else if (route is ShellRoute) {
-      fullpath = parentFullpath;
-    }
+    subPathParameters = <String, String>{};
 
     final RouteMatch? match = RouteMatch.match(
       route: route,
       restLoc: restLoc,
       parentSubloc: parentSubloc,
-      fullpath: fullpath,
-      queryParams: queryParams,
-      queryParametersAll: queryParametersAll,
+      pathParameters: subPathParameters,
       extra: extra,
     );
 
@@ -157,7 +169,7 @@
       // If it is a complete match, then return the matched route
       // NOTE: need a lower case match because subloc is canonicalized to match
       // the path case whereas the location can be of any case and still match
-      result.add(<RouteMatch>[match]);
+      result = <RouteMatch>[match];
     } else if (route.routes.isEmpty) {
       // If it is partial match but no sub-routes, bail.
       continue;
@@ -177,59 +189,48 @@
         newParentSubLoc = match.subloc;
       }
 
-      final List<RouteMatch> subRouteMatch = _getLocRouteRecursively(
+      final List<RouteMatch>? subRouteMatch = _getLocRouteRecursively(
         loc: loc,
         restLoc: childRestLoc,
         parentSubloc: newParentSubLoc,
         routes: route.routes,
-        parentFullpath: fullpath,
-        queryParams: queryParams,
-        queryParametersAll: queryParametersAll,
+        pathParameters: subPathParameters,
         extra: extra,
-      ).toList();
+      );
 
       // If there's no sub-route matches, there is no match for this location
-      if (subRouteMatch.isEmpty) {
+      if (subRouteMatch == null) {
         continue;
       }
-      result.add(<RouteMatch>[match, ...subRouteMatch]);
+      result = <RouteMatch>[match, ...subRouteMatch];
     }
     // Should only reach here if there is a match.
-    if (debugGatherAllMatches) {
-      continue;
-    } else {
-      break;
-    }
+    break;
   }
-
-  if (result.isEmpty) {
-    return <RouteMatch>[];
+  if (result != null) {
+    pathParameters.addAll(subPathParameters);
   }
-
-  // If there are multiple routes that match the location, returning the first one.
-  // To make predefined routes to take precedence over dynamic routes eg. '/:id'
-  // consider adding the dynamic route at the end of the routes
-  return result.first;
+  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(<RouteMatch>[
-    RouteMatch(
-      subloc: uri.path,
-      fullpath: uri.path,
-      encodedParams: <String, String>{},
-      queryParams: uri.queryParameters,
-      queryParametersAll: uri.queryParametersAll,
-      extra: null,
-      error: error,
-      route: GoRoute(
-        path: uri.toString(),
-        pageBuilder: (BuildContext context, GoRouterState state) {
-          throw UnimplementedError();
-        },
-      ),
-    ),
-  ]);
+  return RouteMatchList(
+      <RouteMatch>[
+        RouteMatch(
+          subloc: uri.path,
+          extra: null,
+          error: error,
+          route: GoRoute(
+            path: uri.toString(),
+            pageBuilder: (BuildContext context, GoRouterState state) {
+              throw UnimplementedError();
+            },
+          ),
+          pageKey: const ValueKey<String>('error'),
+        ),
+      ],
+      uri,
+      const <String, String>{});
 }
diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart
index 61a89b8..e15fba3 100644
--- a/packages/go_router/lib/src/parser.dart
+++ b/packages/go_router/lib/src/parser.dart
@@ -62,7 +62,7 @@
 
       // If there is a matching error for the initial location, we should
       // still try to process the top-level redirects.
-      initialMatches = RouteMatchList.empty();
+      initialMatches = RouteMatchList.empty;
     }
     Future<RouteMatchList> processRedirectorResult(RouteMatchList matches) {
       if (matches.isEmpty) {
@@ -99,7 +99,7 @@
   @override
   RouteInformation restoreRouteInformation(RouteMatchList configuration) {
     return RouteInformation(
-      location: configuration.location.toString(),
+      location: configuration.uri.toString(),
       state: configuration.extra,
     );
   }
diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart
index 996aa34..3ebef5c 100644
--- a/packages/go_router/lib/src/redirection.dart
+++ b/packages/go_router/lib/src/redirection.dart
@@ -26,13 +26,13 @@
     {List<RouteMatchList>? redirectHistory,
     Object? extra}) {
   FutureOr<RouteMatchList> processRedirect(RouteMatchList prevMatchList) {
-    final String prevLocation = prevMatchList.location.toString();
+    final String prevLocation = prevMatchList.uri.toString();
     FutureOr<RouteMatchList> processTopLevelRedirect(
         String? topRedirectLocation) {
       if (topRedirectLocation != null && topRedirectLocation != prevLocation) {
         final RouteMatchList newMatch = _getNewMatches(
           topRedirectLocation,
-          prevMatchList.location,
+          prevMatchList.uri,
           configuration,
           matcher,
           redirectHistory!,
@@ -50,24 +50,13 @@
         );
       }
 
-      // Merge new params to keep params from previously matched paths, e.g.
-      // /users/:userId/book/:bookId provides userId and bookId to bookgit /:bookId
-      Map<String, String> previouslyMatchedParams = <String, String>{};
-      for (final RouteMatch match in prevMatchList.matches) {
-        assert(
-          !previouslyMatchedParams.keys.any(match.encodedParams.containsKey),
-          'Duplicated parameter names',
-        );
-        match.encodedParams.addAll(previouslyMatchedParams);
-        previouslyMatchedParams = match.encodedParams;
-      }
       FutureOr<RouteMatchList> processRouteLevelRedirect(
           String? routeRedirectLocation) {
         if (routeRedirectLocation != null &&
             routeRedirectLocation != prevLocation) {
           final RouteMatchList newMatch = _getNewMatches(
             routeRedirectLocation,
-            prevMatchList.location,
+            prevMatchList.uri,
             configuration,
             matcher,
             redirectHistory!,
@@ -99,7 +88,6 @@
 
     redirectHistory ??= <RouteMatchList>[prevMatchList];
     // Check for top-level redirect
-    final Uri uri = prevMatchList.location;
     final FutureOr<String?> topRedirectResult = configuration.topRedirect(
       context,
       GoRouterState(
@@ -108,10 +96,11 @@
         name: null,
         // No name available at the top level trim the query params off the
         // sub-location to match route.redirect
-        subloc: uri.path,
-        queryParams: uri.queryParameters,
-        queryParametersAll: uri.queryParametersAll,
+        subloc: prevMatchList.uri.path,
+        queryParams: prevMatchList.uri.queryParameters,
+        queryParametersAll: prevMatchList.uri.queryParametersAll,
         extra: extra,
+        pageKey: const ValueKey<String>('topLevel'),
       ),
     );
 
@@ -148,15 +137,16 @@
       context,
       GoRouterState(
         configuration,
-        location: matchList.location.toString(),
+        location: matchList.uri.toString(),
         subloc: match.subloc,
         name: route.name,
         path: route.path,
-        fullpath: match.fullpath,
+        fullpath: matchList.fullpath,
         extra: match.extra,
-        params: match.decodedParams,
-        queryParams: match.queryParams,
-        queryParametersAll: match.queryParametersAll,
+        params: matchList.pathParameters,
+        queryParams: matchList.uri.queryParameters,
+        queryParametersAll: matchList.uri.queryParametersAll,
+        pageKey: match.pageKey,
       ),
     );
   }
@@ -216,8 +206,8 @@
 
   @override
   String toString() => '${super.toString()} ${<String>[
-        ...matches.map(
-            (RouteMatchList routeMatches) => routeMatches.location.toString()),
+        ...matches
+            .map((RouteMatchList routeMatches) => routeMatches.uri.toString()),
       ].join(' => ')}';
 }
 
diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart
index f1442b4..5e58f0c 100644
--- a/packages/go_router/lib/src/route.dart
+++ b/packages/go_router/lib/src/route.dart
@@ -2,8 +2,8 @@
 // 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/widgets.dart';
+import 'package:meta/meta.dart';
 
 import 'configuration.dart';
 import 'pages/custom_transition_page.dart';
@@ -131,40 +131,14 @@
     this.pageBuilder,
     this.parentNavigatorKey,
     this.redirect,
-    List<RouteBase> routes = const <RouteBase>[],
+    super.routes = const <RouteBase>[],
   })  : assert(path.isNotEmpty, 'GoRoute path cannot be empty'),
         assert(name == null || name.isNotEmpty, 'GoRoute name cannot be empty'),
         assert(pageBuilder != null || builder != null || redirect != null,
             'builder, pageBuilder, or redirect must be provided'),
-        super._(
-          routes: routes,
-        ) {
+        super._() {
     // cache the path regexp and parameters
-    _pathRE = patternToRegExp(path, _pathParams);
-    assert(() {
-      // check path params
-      final Map<String, List<String>> groupedParams =
-          _pathParams.groupListsBy<String>((String p) => p);
-      final Map<String, List<String>> dupParams =
-          Map<String, List<String>>.fromEntries(
-        groupedParams.entries
-            .where((MapEntry<String, List<String>> e) => e.value.length > 1),
-      );
-      assert(dupParams.isEmpty,
-          'duplicate path params: ${dupParams.keys.join(', ')}');
-
-      // check sub-routes
-      for (final RouteBase route in routes) {
-        // check paths
-        if (route is GoRoute) {
-          assert(
-              route.path == '/' ||
-                  (!route.path.startsWith('/') && !route.path.endsWith('/')),
-              'sub-route path may not start or end with /: ${route.path}');
-        }
-      }
-      return true;
-    }());
+    _pathRE = patternToRegExp(path, pathParams);
   }
 
   /// Optional name of the route.
@@ -332,9 +306,16 @@
 
   /// Extract the path parameters from a match.
   Map<String, String> extractPathParams(RegExpMatch match) =>
-      extractPathParameters(_pathParams, match);
+      extractPathParameters(pathParams, match);
 
-  final List<String> _pathParams = <String>[];
+  /// The path parameters in this route.
+  @internal
+  final List<String> pathParams = <String>[];
+
+  @override
+  String toString() {
+    return 'GoRoute(name: $name, path: $path)';
+  }
 
   late final RegExp _pathRE;
 }
diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart
index fded5fa..120bfd2 100644
--- a/packages/go_router/lib/src/router.dart
+++ b/packages/go_router/lib/src/router.dart
@@ -146,8 +146,18 @@
   bool canPop() => _routerDelegate.canPop();
 
   void _handleStateMayChange() {
-    final String newLocation =
-        _routerDelegate.currentConfiguration.location.toString();
+    final String newLocation;
+    if (routerDelegate.currentConfiguration.isNotEmpty &&
+        routerDelegate.currentConfiguration.matches.last
+            is ImperativeRouteMatch) {
+      newLocation = (routerDelegate.currentConfiguration.matches.last
+              as ImperativeRouteMatch)
+          .matches
+          .uri
+          .toString();
+    } else {
+      newLocation = _routerDelegate.currentConfiguration.uri.toString();
+    }
     if (_location != newLocation) {
       _location = newLocation;
       notifyListeners();
@@ -207,7 +217,7 @@
       _routerDelegate.navigatorKey.currentContext!,
     )
         .then<void>((RouteMatchList matches) {
-      _routerDelegate.push(matches.last);
+      _routerDelegate.push(matches);
     });
   }
 
@@ -239,7 +249,7 @@
       _routerDelegate.navigatorKey.currentContext!,
     )
         .then<void>((RouteMatchList matchList) {
-      routerDelegate.replace(matchList.matches.last);
+      routerDelegate.replace(matchList);
     });
   }
 
diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart
index 5197db7..721565a 100644
--- a/packages/go_router/lib/src/state.dart
+++ b/packages/go_router/lib/src/state.dart
@@ -14,7 +14,7 @@
 @immutable
 class GoRouterState {
   /// Default constructor for creating route state during routing.
-  GoRouterState(
+  const GoRouterState(
     this._configuration, {
     required this.location,
     required this.subloc,
@@ -26,13 +26,8 @@
     this.queryParametersAll = const <String, List<String>>{},
     this.extra,
     this.error,
-    ValueKey<String>? pageKey,
-  }) : pageKey = pageKey ??
-            ValueKey<String>(error != null
-                ? 'error'
-                : fullpath != null && fullpath.isNotEmpty
-                    ? fullpath
-                    : subloc);
+    required this.pageKey,
+  });
 
   // TODO(johnpryan): remove once namedLocation is removed from go_router.
   // See https://github.com/flutter/flutter/issues/107729
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index adb22ee..5655bd6 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: 5.1.10
+version: 5.2.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 53c0d87..ba4f7f5 100644
--- a/packages/go_router/test/builder_test.dart
+++ b/packages/go_router/test/builder_test.dart
@@ -28,18 +28,18 @@
         navigatorKey: GlobalKey<NavigatorState>(),
       );
 
-      final RouteMatchList matches = RouteMatchList(<RouteMatch>[
-        RouteMatch(
-          route: config.routes.first as GoRoute,
-          subloc: '/',
-          fullpath: '/',
-          encodedParams: <String, String>{},
-          queryParams: <String, String>{},
-          queryParametersAll: <String, List<String>>{},
-          extra: null,
-          error: null,
-        ),
-      ]);
+      final RouteMatchList matches = RouteMatchList(
+          <RouteMatch>[
+            RouteMatch(
+              route: config.routes.first as GoRoute,
+              subloc: '/',
+              extra: null,
+              error: null,
+              pageKey: const ValueKey<String>('/'),
+            ),
+          ],
+          Uri.parse('/'),
+          const <String, String>{});
 
       await tester.pumpWidget(
         _BuilderTestWidget(
@@ -75,18 +75,18 @@
         navigatorKey: GlobalKey<NavigatorState>(),
       );
 
-      final RouteMatchList matches = RouteMatchList(<RouteMatch>[
-        RouteMatch(
-          route: config.routes.first,
-          subloc: '/',
-          fullpath: '/',
-          encodedParams: <String, String>{},
-          queryParams: <String, String>{},
-          queryParametersAll: <String, List<String>>{},
-          extra: null,
-          error: null,
-        ),
-      ]);
+      final RouteMatchList matches = RouteMatchList(
+          <RouteMatch>[
+            RouteMatch(
+              route: config.routes.first,
+              subloc: '/',
+              extra: null,
+              error: null,
+              pageKey: const ValueKey<String>('/'),
+            ),
+          ],
+          Uri.parse('/'),
+          <String, String>{});
 
       await tester.pumpWidget(
         _BuilderTestWidget(
@@ -117,18 +117,18 @@
         },
       );
 
-      final RouteMatchList matches = RouteMatchList(<RouteMatch>[
-        RouteMatch(
-          route: config.routes.first as GoRoute,
-          subloc: '/',
-          fullpath: '/',
-          encodedParams: <String, String>{},
-          queryParams: <String, String>{},
-          queryParametersAll: <String, List<String>>{},
-          extra: null,
-          error: null,
-        ),
-      ]);
+      final RouteMatchList matches = RouteMatchList(
+          <RouteMatch>[
+            RouteMatch(
+              route: config.routes.first as GoRoute,
+              subloc: '/',
+              extra: null,
+              error: null,
+              pageKey: const ValueKey<String>('/'),
+            ),
+          ],
+          Uri.parse('/'),
+          <String, String>{});
 
       await tester.pumpWidget(
         _BuilderTestWidget(
@@ -172,28 +172,25 @@
         },
       );
 
-      final RouteMatchList matches = RouteMatchList(<RouteMatch>[
-        RouteMatch(
-          route: config.routes.first,
-          subloc: '',
-          fullpath: '',
-          encodedParams: <String, String>{},
-          queryParams: <String, String>{},
-          queryParametersAll: <String, List<String>>{},
-          extra: null,
-          error: null,
-        ),
-        RouteMatch(
-          route: config.routes.first.routes.first,
-          subloc: '/details',
-          fullpath: '/details',
-          encodedParams: <String, String>{},
-          queryParams: <String, String>{},
-          queryParametersAll: <String, List<String>>{},
-          extra: null,
-          error: null,
-        ),
-      ]);
+      final RouteMatchList matches = RouteMatchList(
+          <RouteMatch>[
+            RouteMatch(
+              route: config.routes.first,
+              subloc: '',
+              extra: null,
+              error: null,
+              pageKey: const ValueKey<String>(''),
+            ),
+            RouteMatch(
+              route: config.routes.first.routes.first,
+              subloc: '/details',
+              extra: null,
+              error: null,
+              pageKey: const ValueKey<String>('/details'),
+            ),
+          ],
+          Uri.parse('/details'),
+          <String, String>{});
 
       await tester.pumpWidget(
         _BuilderTestWidget(
@@ -250,18 +247,18 @@
         },
       );
 
-      final RouteMatchList matches = RouteMatchList(<RouteMatch>[
-        RouteMatch(
-          route: config.routes.first.routes.first as GoRoute,
-          subloc: '/a/details',
-          fullpath: '/a/details',
-          encodedParams: <String, String>{},
-          queryParams: <String, String>{},
-          queryParametersAll: <String, List<String>>{},
-          extra: null,
-          error: null,
-        ),
-      ]);
+      final RouteMatchList matches = RouteMatchList(
+          <RouteMatch>[
+            RouteMatch(
+              route: config.routes.first.routes.first as GoRoute,
+              subloc: '/a/details',
+              extra: null,
+              error: null,
+              pageKey: const ValueKey<String>('/a/details'),
+            ),
+          ],
+          Uri.parse('/a/details'),
+          <String, String>{});
 
       await tester.pumpWidget(
         _BuilderTestWidget(
diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart
index 61110b4..3974223 100644
--- a/packages/go_router/test/delegate_test.dart
+++ b/packages/go_router/test/delegate_test.dart
@@ -5,6 +5,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/go_router.dart';
+import 'package:go_router/src/delegate.dart';
 import 'package:go_router/src/match.dart';
 import 'package:go_router/src/misc/error_screen.dart';
 
@@ -62,10 +63,6 @@
       (WidgetTester tester) async {
         final GoRouter goRouter = await createGoRouter(tester);
         expect(goRouter.routerDelegate.matches.matches.length, 1);
-        expect(
-          goRouter.routerDelegate.matches.matches[0].pageKey,
-          null,
-        );
 
         goRouter.push('/a');
         await tester.pumpAndSettle();
@@ -113,47 +110,49 @@
   });
 
   group('replace', () {
-    testWidgets(
-      'It should replace the last match with the given one',
-      (WidgetTester tester) async {
-        final GoRouter goRouter = GoRouter(
-          initialLocation: '/',
-          routes: <GoRoute>[
-            GoRoute(path: '/', builder: (_, __) => const SizedBox()),
-            GoRoute(path: '/page-0', builder: (_, __) => const SizedBox()),
-            GoRoute(path: '/page-1', builder: (_, __) => const SizedBox()),
-          ],
-        );
-        await tester.pumpWidget(
-          MaterialApp.router(
-            routerConfig: goRouter,
-          ),
-        );
+    testWidgets('It should replace the last match with the given one',
+        (WidgetTester tester) async {
+      final GoRouter goRouter = GoRouter(
+        initialLocation: '/',
+        routes: <GoRoute>[
+          GoRoute(path: '/', builder: (_, __) => const SizedBox()),
+          GoRoute(path: '/page-0', builder: (_, __) => const SizedBox()),
+          GoRoute(path: '/page-1', builder: (_, __) => const SizedBox()),
+        ],
+      );
+      await tester.pumpWidget(
+        MaterialApp.router(
+          routerConfig: goRouter,
+        ),
+      );
 
-        goRouter.push('/page-0');
+      goRouter.push('/page-0');
 
-        goRouter.routerDelegate.addListener(expectAsync0(() {}));
-        final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
-        final RouteMatch last = goRouter.routerDelegate.matches.last;
-        goRouter.replace('/page-1');
-        expect(goRouter.routerDelegate.matches.matches.length, 2);
-        expect(
-          goRouter.routerDelegate.matches.matches.first,
-          first,
-          reason: 'The first match should still be in the list of matches',
-        );
-        expect(
-          goRouter.routerDelegate.matches.last,
-          isNot(last),
-          reason: 'The last match should have been removed',
-        );
-        expect(
-          goRouter.routerDelegate.matches.last.fullpath,
-          '/page-1',
-          reason: 'The new location should have been pushed',
-        );
-      },
-    );
+      goRouter.routerDelegate.addListener(expectAsync0(() {}));
+      final RouteMatch first = goRouter.routerDelegate.matches.matches.first;
+      final RouteMatch last = goRouter.routerDelegate.matches.last;
+      goRouter.replace('/page-1');
+      expect(goRouter.routerDelegate.matches.matches.length, 2);
+      expect(
+        goRouter.routerDelegate.matches.matches.first,
+        first,
+        reason: 'The first match should still be in the list of matches',
+      );
+      expect(
+        goRouter.routerDelegate.matches.last,
+        isNot(last),
+        reason: 'The last match should have been removed',
+      );
+      expect(
+        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch)
+            .matches
+            .uri
+            .toString(),
+        '/page-1',
+        reason: 'The new location should have been pushed',
+      );
+    });
+
     testWidgets(
       'It should return different pageKey when replace is called',
       (WidgetTester tester) async {
@@ -161,7 +160,7 @@
         expect(goRouter.routerDelegate.matches.matches.length, 1);
         expect(
           goRouter.routerDelegate.matches.matches[0].pageKey,
-          null,
+          isNotNull,
         );
 
         goRouter.push('/a');
@@ -228,17 +227,11 @@
         );
         expect(
           goRouter.routerDelegate.matches.last,
-          isA<RouteMatch>()
-              .having(
-                (RouteMatch match) => match.fullpath,
-                'match.fullpath',
-                '/page-1',
-              )
-              .having(
-                (RouteMatch match) => (match.route as GoRoute).name,
-                'match.route.name',
-                'page1',
-              ),
+          isA<RouteMatch>().having(
+            (RouteMatch match) => (match.route as GoRoute).name,
+            'match.route.name',
+            'page1',
+          ),
           reason: 'The new location should have been pushed',
         );
       },
diff --git a/packages/go_router/test/go_router_state_test.dart b/packages/go_router/test/go_router_state_test.dart
index c805aa9..65a0017 100644
--- a/packages/go_router/test/go_router_state_test.dart
+++ b/packages/go_router/test/go_router_state_test.dart
@@ -42,7 +42,7 @@
             path: '/',
             builder: (_, __) {
               return Builder(builder: (BuildContext context) {
-                return Text(GoRouterState.of(context).location);
+                return Text('1 ${GoRouterState.of(context).location}');
               });
             },
             routes: <GoRoute>[
@@ -50,7 +50,7 @@
                   path: 'a',
                   builder: (_, __) {
                     return Builder(builder: (BuildContext context) {
-                      return Text(GoRouterState.of(context).location);
+                      return Text('2 ${GoRouterState.of(context).location}');
                     });
                   }),
             ]),
@@ -58,13 +58,13 @@
       final GoRouter router = await createRouter(routes, tester);
       router.go('/?p=123');
       await tester.pumpAndSettle();
-      expect(find.text('/?p=123'), findsOneWidget);
+      expect(find.text('1 /?p=123'), findsOneWidget);
 
       router.go('/a');
       await tester.pumpAndSettle();
-      expect(find.text('/a'), findsOneWidget);
+      expect(find.text('2 /a'), findsOneWidget);
       // The query parameter is removed, so is the location in first page.
-      expect(find.text('/', skipOffstage: false), findsOneWidget);
+      expect(find.text('1 /a', skipOffstage: false), findsOneWidget);
     });
 
     testWidgets('registry retains GoRouterState for exiting route',
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index 96df8e7..d301742 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -9,7 +9,9 @@
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/go_router.dart';
+import 'package:go_router/src/delegate.dart';
 import 'package:go_router/src/match.dart';
+import 'package:go_router/src/matching.dart';
 import 'package:logging/logging.dart';
 
 import 'test_helpers.dart';
@@ -43,24 +45,24 @@
       ];
 
       final GoRouter router = await createRouter(routes, tester);
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
-      expect(matches.first.fullpath, '/');
+      final RouteMatchList matches = router.routerDelegate.matches;
+      expect(matches.matches, hasLength(1));
+      expect(matches.uri.toString(), '/');
       expect(find.byType(HomeScreen), findsOneWidget);
     });
 
     testWidgets('If there is more than one route to match, use the first match',
         (WidgetTester tester) async {
       final List<GoRoute> routes = <GoRoute>[
-        GoRoute(path: '/', builder: dummy),
-        GoRoute(path: '/', builder: dummy),
+        GoRoute(name: '1', path: '/', builder: dummy),
+        GoRoute(name: '2', path: '/', builder: dummy),
       ];
 
       final GoRouter router = await createRouter(routes, tester);
       router.go('/');
       final List<RouteMatch> matches = router.routerDelegate.matches.matches;
       expect(matches, hasLength(1));
-      expect(matches.first.fullpath, '/');
+      expect((matches.first.route as GoRoute).name, '1');
       expect(find.byType(DummyScreen), findsOneWidget);
     });
 
@@ -72,13 +74,17 @@
 
     test('leading / on sub-route', () {
       expect(() {
-        GoRoute(
-          path: '/',
-          builder: dummy,
-          routes: <GoRoute>[
+        GoRouter(
+          routes: <RouteBase>[
             GoRoute(
-              path: '/foo',
+              path: '/',
               builder: dummy,
+              routes: <GoRoute>[
+                GoRoute(
+                  path: '/foo',
+                  builder: dummy,
+                ),
+              ],
             ),
           ],
         );
@@ -87,13 +93,17 @@
 
     test('trailing / on sub-route', () {
       expect(() {
-        GoRoute(
-          path: '/',
-          builder: dummy,
-          routes: <GoRoute>[
+        GoRouter(
+          routes: <RouteBase>[
             GoRoute(
-              path: 'foo/',
+              path: '/',
               builder: dummy,
+              routes: <GoRoute>[
+                GoRoute(
+                  path: 'foo/',
+                  builder: dummy,
+                ),
+              ],
             ),
           ],
         );
@@ -328,44 +338,44 @@
 
       final GoRouter router = await createRouter(routes, tester);
       {
-        final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-        expect(matches, hasLength(1));
-        expect(matches.first.fullpath, '/');
+        final RouteMatchList matches = router.routerDelegate.matches;
+        expect(matches.matches, hasLength(1));
+        expect(matches.uri.toString(), '/');
         expect(find.byType(HomeScreen), findsOneWidget);
       }
 
       router.go('/login');
       await tester.pumpAndSettle();
       {
-        final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-        expect(matches.length, 2);
-        expect(matches.first.subloc, '/');
+        final RouteMatchList matches = router.routerDelegate.matches;
+        expect(matches.matches.length, 2);
+        expect(matches.matches.first.subloc, '/');
         expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
-        expect(matches[1].subloc, '/login');
+        expect(matches.matches[1].subloc, '/login');
         expect(find.byType(LoginScreen), findsOneWidget);
       }
 
       router.go('/family/f2');
       await tester.pumpAndSettle();
       {
-        final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-        expect(matches.length, 2);
-        expect(matches.first.subloc, '/');
+        final RouteMatchList matches = router.routerDelegate.matches;
+        expect(matches.matches.length, 2);
+        expect(matches.matches.first.subloc, '/');
         expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
-        expect(matches[1].subloc, '/family/f2');
+        expect(matches.matches[1].subloc, '/family/f2');
         expect(find.byType(FamilyScreen), findsOneWidget);
       }
 
       router.go('/family/f2/person/p1');
       await tester.pumpAndSettle();
       {
-        final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-        expect(matches.length, 3);
-        expect(matches.first.subloc, '/');
+        final RouteMatchList matches = router.routerDelegate.matches;
+        expect(matches.matches.length, 3);
+        expect(matches.matches.first.subloc, '/');
         expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
-        expect(matches[1].subloc, '/family/f2');
+        expect(matches.matches[1].subloc, '/family/f2');
         expect(find.byType(FamilyScreen, skipOffstage: false), findsOneWidget);
-        expect(matches[2].subloc, '/family/f2/person/p1');
+        expect(matches.matches[2].subloc, '/family/f2/person/p1');
         expect(find.byType(PersonScreen), findsOneWidget);
       }
     });
@@ -1134,14 +1144,12 @@
       final GoRouter router = await createRouter(routes, tester);
       final String loc = router
           .namedLocation('page1', params: <String, String>{'param1': param1});
-      log.info('loc= $loc');
       router.go(loc);
       await tester.pumpAndSettle();
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      log.info('param1= ${matches.first.decodedParams['param1']}');
+      final RouteMatchList matches = router.routerDelegate.matches;
       expect(find.byType(DummyScreen), findsOneWidget);
-      expect(matches.first.decodedParams['param1'], param1);
+      expect(matches.pathParameters['param1'], param1);
     });
 
     testWidgets('preserve query param spaces and slashes',
@@ -1163,9 +1171,9 @@
           queryParams: <String, String>{'param1': param1});
       router.go(loc);
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final RouteMatchList matches = router.routerDelegate.matches;
       expect(find.byType(DummyScreen), findsOneWidget);
-      expect(matches.first.queryParams['param1'], param1);
+      expect(matches.uri.queryParameters['param1'], param1);
     });
   });
 
@@ -1835,12 +1843,12 @@
         final String loc = '/family/$fid';
         router.go(loc);
         await tester.pumpAndSettle();
-        final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+        final RouteMatchList matches = router.routerDelegate.matches;
 
         expect(router.location, loc);
-        expect(matches, hasLength(1));
+        expect(matches.matches, hasLength(1));
         expect(find.byType(FamilyScreen), findsOneWidget);
-        expect(matches.first.decodedParams['fid'], fid);
+        expect(matches.pathParameters['fid'], fid);
       }
     });
 
@@ -1864,12 +1872,12 @@
         final String loc = '/family?fid=$fid';
         router.go(loc);
         await tester.pumpAndSettle();
-        final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+        final RouteMatchList matches = router.routerDelegate.matches;
 
         expect(router.location, loc);
-        expect(matches, hasLength(1));
+        expect(matches.matches, hasLength(1));
         expect(find.byType(FamilyScreen), findsOneWidget);
-        expect(matches.first.queryParams['fid'], fid);
+        expect(matches.uri.queryParameters['fid'], fid);
       }
     });
 
@@ -1891,10 +1899,9 @@
       router.go(loc);
       await tester.pumpAndSettle();
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      log.info('param1= ${matches.first.decodedParams['param1']}');
+      final RouteMatchList matches = router.routerDelegate.matches;
       expect(find.byType(DummyScreen), findsOneWidget);
-      expect(matches.first.decodedParams['param1'], param1);
+      expect(matches.pathParameters['param1'], param1);
     });
 
     testWidgets('preserve query param spaces and slashes',
@@ -1914,17 +1921,17 @@
       router.go('/page1?param1=$param1');
       await tester.pumpAndSettle();
 
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final RouteMatchList matches = router.routerDelegate.matches;
       expect(find.byType(DummyScreen), findsOneWidget);
-      expect(matches.first.queryParams['param1'], param1);
+      expect(matches.uri.queryParameters['param1'], param1);
 
       final String loc = '/page1?param1=${Uri.encodeQueryComponent(param1)}';
       router.go(loc);
       await tester.pumpAndSettle();
 
-      final List<RouteMatch> matches2 = router.routerDelegate.matches.matches;
+      final RouteMatchList matches2 = router.routerDelegate.matches;
       expect(find.byType(DummyScreen), findsOneWidget);
-      expect(matches2[0].queryParams['param1'], param1);
+      expect(matches2.uri.queryParameters['param1'], param1);
     });
 
     test('error: duplicate path param', () {
@@ -1963,9 +1970,9 @@
         tester,
         initialLocation: '/?id=0&id=1',
       );
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
-      expect(matches.first.fullpath, '/');
+      final RouteMatchList matches = router.routerDelegate.matches;
+      expect(matches.matches, hasLength(1));
+      expect(matches.fullpath, '/');
       expect(find.byType(HomeScreen), findsOneWidget);
     });
 
@@ -1986,9 +1993,9 @@
 
       router.go('/0?id=1');
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
-      expect(matches, hasLength(1));
-      expect(matches.first.fullpath, '/:id');
+      final RouteMatchList matches = router.routerDelegate.matches;
+      expect(matches.matches, hasLength(1));
+      expect(matches.fullpath, '/:id');
       expect(find.byType(HomeScreen), findsOneWidget);
     });
 
@@ -2098,13 +2105,15 @@
 
       router.push(loc);
       await tester.pumpAndSettle();
-      final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+      final RouteMatchList matches = router.routerDelegate.matches;
 
       expect(router.location, loc);
-      expect(matches, hasLength(2));
+      expect(matches.matches, hasLength(2));
       expect(find.byType(PersonScreen), findsOneWidget);
-      expect(matches.last.decodedParams['fid'], fid);
-      expect(matches.last.decodedParams['pid'], pid);
+      final ImperativeRouteMatch imperativeRouteMatch =
+          matches.matches.last as ImperativeRouteMatch;
+      expect(imperativeRouteMatch.matches.pathParameters['fid'], fid);
+      expect(imperativeRouteMatch.matches.pathParameters['pid'], pid);
     });
 
     testWidgets('goNames should allow dynamics values for queryParams',
diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart
index 69e6eac..aa14db1 100644
--- a/packages/go_router/test/match_test.dart
+++ b/packages/go_router/test/match_test.dart
@@ -14,47 +14,36 @@
         path: '/users/:userId',
         builder: _builder,
       );
+      final Map<String, String> pathParameters = <String, String>{};
       final RouteMatch? match = RouteMatch.match(
         route: route,
         restLoc: '/users/123',
         parentSubloc: '',
-        fullpath: '/users/:userId',
-        queryParams: <String, String>{},
+        pathParameters: pathParameters,
         extra: const _Extra('foo'),
-        queryParametersAll: <String, List<String>>{
-          'bar': <String>['baz', 'biz'],
-        },
       );
       if (match == null) {
         fail('Null match');
       }
       expect(match.route, route);
       expect(match.subloc, '/users/123');
-      expect(match.fullpath, '/users/:userId');
-      expect(match.encodedParams['userId'], '123');
-      expect(match.queryParams['foo'], isNull);
-      expect(match.queryParametersAll['bar'], <String>['baz', 'biz']);
+      expect(pathParameters['userId'], '123');
       expect(match.extra, const _Extra('foo'));
       expect(match.error, isNull);
-      expect(match.pageKey, isNull);
-      expect(match.fullUriString, '/users/123?bar=baz&bar=biz');
+      expect(match.pageKey, isNotNull);
     });
+
     test('subloc', () {
       final GoRoute route = GoRoute(
         path: 'users/:userId',
         builder: _builder,
       );
+      final Map<String, String> pathParameters = <String, String>{};
       final RouteMatch? match = RouteMatch.match(
         route: route,
         restLoc: 'users/123',
         parentSubloc: '/home',
-        fullpath: '/home/users/:userId',
-        queryParams: <String, String>{
-          'foo': 'bar',
-        },
-        queryParametersAll: <String, List<String>>{
-          'foo': <String>['bar'],
-        },
+        pathParameters: pathParameters,
         extra: const _Extra('foo'),
       );
       if (match == null) {
@@ -62,14 +51,12 @@
       }
       expect(match.route, route);
       expect(match.subloc, '/home/users/123');
-      expect(match.fullpath, '/home/users/:userId');
-      expect(match.encodedParams['userId'], '123');
-      expect(match.queryParams['foo'], 'bar');
+      expect(pathParameters['userId'], '123');
       expect(match.extra, const _Extra('foo'));
       expect(match.error, isNull);
-      expect(match.pageKey, isNull);
-      expect(match.fullUriString, '/home/users/123?foo=bar');
+      expect(match.pageKey, isNotNull);
     });
+
     test('ShellRoute has a unique pageKey', () {
       final ShellRoute route = ShellRoute(
         builder: _shellBuilder,
@@ -80,17 +67,12 @@
           ),
         ],
       );
+      final Map<String, String> pathParameters = <String, String>{};
       final RouteMatch? match = RouteMatch.match(
         route: route,
         restLoc: 'users/123',
         parentSubloc: '/home',
-        fullpath: '/home/users/:userId',
-        queryParams: <String, String>{
-          'foo': 'bar',
-        },
-        queryParametersAll: <String, List<String>>{
-          'foo': <String>['bar'],
-        },
+        pathParameters: pathParameters,
         extra: const _Extra('foo'),
       );
       if (match == null) {
@@ -98,6 +80,61 @@
       }
       expect(match.pageKey, isNotNull);
     });
+
+    test('ShellRoute Match has stable unique key', () {
+      final ShellRoute route = ShellRoute(
+        builder: _shellBuilder,
+        routes: <GoRoute>[
+          GoRoute(
+            path: '/users/:userId',
+            builder: _builder,
+          ),
+        ],
+      );
+      final Map<String, String> pathParameters = <String, String>{};
+      final RouteMatch? match1 = RouteMatch.match(
+        route: route,
+        restLoc: 'users/123',
+        parentSubloc: '/home',
+        pathParameters: pathParameters,
+        extra: const _Extra('foo'),
+      );
+
+      final RouteMatch? match2 = RouteMatch.match(
+        route: route,
+        restLoc: 'users/1234',
+        parentSubloc: '/home',
+        pathParameters: pathParameters,
+        extra: const _Extra('foo1'),
+      );
+
+      expect(match1!.pageKey, match2!.pageKey);
+    });
+
+    test('GoRoute Match has stable unique key', () {
+      final GoRoute route = GoRoute(
+        path: 'users/:userId',
+        builder: _builder,
+      );
+      final Map<String, String> pathParameters = <String, String>{};
+      final RouteMatch? match1 = RouteMatch.match(
+        route: route,
+        restLoc: 'users/123',
+        parentSubloc: '/home',
+        pathParameters: pathParameters,
+        extra: const _Extra('foo'),
+      );
+
+      final RouteMatch? match2 = RouteMatch.match(
+        route: route,
+        restLoc: 'users/1234',
+        parentSubloc: '/home',
+        pathParameters: pathParameters,
+        extra: const _Extra('foo1'),
+      );
+
+      expect(match1!.pageKey, match2!.pageKey);
+    });
   });
 }
 
diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart
index 6e3c0c6..cac9a55 100644
--- a/packages/go_router/test/parser_test.dart
+++ b/packages/go_router/test/parser_test.dart
@@ -56,9 +56,8 @@
             const RouteInformation(location: '/'), context);
     List<RouteMatch> matches = matchesObj.matches;
     expect(matches.length, 1);
-    expect(matches[0].queryParams.isEmpty, isTrue);
+    expect(matchesObj.uri.toString(), '/');
     expect(matches[0].extra, isNull);
-    expect(matches[0].fullUriString, '/');
     expect(matches[0].subloc, '/');
     expect(matches[0].route, routes[0]);
 
@@ -67,17 +66,12 @@
         RouteInformation(location: '/abc?def=ghi', state: extra), context);
     matches = matchesObj.matches;
     expect(matches.length, 2);
-    expect(matches[0].queryParams.length, 1);
-    expect(matches[0].queryParams['def'], 'ghi');
+    expect(matchesObj.uri.toString(), '/abc?def=ghi');
     expect(matches[0].extra, extra);
-    expect(matches[0].fullUriString, '/?def=ghi');
     expect(matches[0].subloc, '/');
     expect(matches[0].route, routes[0]);
 
-    expect(matches[1].queryParams.length, 1);
-    expect(matches[1].queryParams['def'], 'ghi');
     expect(matches[1].extra, extra);
-    expect(matches[1].fullUriString, '/abc?def=ghi');
     expect(matches[1].subloc, '/abc');
     expect(matches[1].route, routes[0].routes[0]);
   });
@@ -195,9 +189,8 @@
             const RouteInformation(location: '/def'), context);
     final List<RouteMatch> matches = matchesObj.matches;
     expect(matches.length, 1);
-    expect(matches[0].queryParams.isEmpty, isTrue);
+    expect(matchesObj.uri.toString(), '/def');
     expect(matches[0].extra, isNull);
-    expect(matches[0].fullUriString, '/def');
     expect(matches[0].subloc, '/def');
     expect(matches[0].error!.toString(),
         'Exception: no routes for location: /def');
@@ -231,18 +224,15 @@
     final List<RouteMatch> matches = matchesObj.matches;
 
     expect(matches.length, 2);
-    expect(matches[0].queryParams.isEmpty, isTrue);
+    expect(matchesObj.uri.toString(), '/123/family/456');
+    expect(matchesObj.pathParameters.length, 2);
+    expect(matchesObj.pathParameters['uid'], '123');
+    expect(matchesObj.pathParameters['fid'], '456');
     expect(matches[0].extra, isNull);
-    expect(matches[0].fullUriString, '/');
     expect(matches[0].subloc, '/');
 
-    expect(matches[1].queryParams.isEmpty, isTrue);
     expect(matches[1].extra, isNull);
-    expect(matches[1].fullUriString, '/123/family/456');
     expect(matches[1].subloc, '/123/family/456');
-    expect(matches[1].encodedParams.length, 2);
-    expect(matches[1].encodedParams['uid'], '123');
-    expect(matches[1].encodedParams['fid'], '456');
   });
 
   testWidgets(
@@ -279,10 +269,9 @@
     final List<RouteMatch> matches = matchesObj.matches;
 
     expect(matches.length, 2);
-    expect(matches[0].fullUriString, '/');
+    expect(matchesObj.uri.toString(), '/123/family/345');
     expect(matches[0].subloc, '/');
 
-    expect(matches[1].fullUriString, '/123/family/345');
     expect(matches[1].subloc, '/123/family/345');
   });
 
@@ -320,10 +309,9 @@
     final List<RouteMatch> matches = matchesObj.matches;
 
     expect(matches.length, 2);
-    expect(matches[0].fullUriString, '/');
+    expect(matchesObj.uri.toString(), '/123/family/345');
     expect(matches[0].subloc, '/');
 
-    expect(matches[1].fullUriString, '/123/family/345');
     expect(matches[1].subloc, '/123/family/345');
   });