[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');
});