[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
This commit is contained in:
chunhtai
2022-11-21 15:36:24 -08:00
committed by GitHub
parent e5c16e0740
commit 92c60ee420
18 changed files with 530 additions and 578 deletions

View File

@ -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.

View File

@ -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 @@ class RouteBuilder {
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 @@ class RouteBuilder {
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 @@ class RouteBuilder {
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 @@ class RouteBuilder {
}
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 @@ class RouteBuilder {
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 @@ class RouteBuilder {
// 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 @@ class RouteBuilder {
/// 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 @@ class RouteBuilder {
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

View File

@ -6,9 +6,9 @@ import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'logging.dart';
import 'misc/errors.dart';
import 'path_utils.dart';
import 'typedefs.dart';
export 'route.dart';
export 'state.dart';
@ -20,34 +20,42 @@ class RouteConfiguration {
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('/')) {
assert(route.path.startsWith('/'),
'top-level path must start with "/": ${route.path}');
} else if (route is ShellRoute) {
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}');
'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(
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;
final GlobalKey<NavigatorState>? parentKey = route.parentNavigatorKey;
if (parentKey != null) {
// Verify that the root navigator or a ShellRoute ancestor has a
// matching navigator key.
@ -57,7 +65,7 @@ class RouteConfiguration {
" an ancestor ShellRoute's navigatorKey or GoRouter's"
' navigatorKey');
checkParentNavigatorKeys(
_debugCheckParentNavigatorKeys(
route.routes,
<GlobalKey<NavigatorState>>[
// Once a parentNavigatorKey is used, only that navigator key
@ -66,7 +74,7 @@ class RouteConfiguration {
],
);
} else {
checkParentNavigatorKeys(
_debugCheckParentNavigatorKeys(
route.routes,
<GlobalKey<NavigatorState>>[
...allowedKeys,
@ -74,20 +82,33 @@ class RouteConfiguration {
);
}
} else if (route is ShellRoute && route.navigatorKey != null) {
checkParentNavigatorKeys(
_debugCheckParentNavigatorKeys(
route.routes,
<GlobalKey<NavigatorState>>[
...allowedKeys..add(route.navigatorKey)
],
<GlobalKey<NavigatorState>>[...allowedKeys..add(route.navigatorKey)],
);
}
}
return true;
}
checkParentNavigatorKeys(
routes, <GlobalKey<NavigatorState>>[navigatorKey]);
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].

View File

@ -11,7 +11,6 @@ import 'builder.dart';
import 'configuration.dart';
import 'match.dart';
import 'matching.dart';
import 'misc/errors.dart';
import 'typedefs.dart';
/// GoRouter implementation of [RouterDelegate].
@ -44,7 +43,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
/// 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 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
}
/// 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 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
///
/// 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 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
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;
}

View File

@ -15,46 +15,25 @@ class RouteMatch {
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 @@ class RouteMatch {
}
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 @@ class RouteMatch {
/// 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 @@ class RouteMatch {
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;
}

View File

@ -18,27 +18,27 @@ class RouteMatcher {
/// 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 @@ class RouteMatcher {
/// 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 @@ class MatcherError extends Error {
}
}
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 @@ List<RouteMatch> _getLocRouteRecursively({
// 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,51 +189,37 @@ List<RouteMatch> _getLocRouteRecursively({
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;
}
if (result != null) {
pathParameters.addAll(subPathParameters);
}
if (result.isEmpty) {
return <RouteMatch>[];
}
// 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>[
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(
@ -230,6 +228,9 @@ RouteMatchList errorScreen(Uri uri, String errorMessage) {
throw UnimplementedError();
},
),
pageKey: const ValueKey<String>('error'),
),
]);
],
uri,
const <String, String>{});
}

View File

@ -62,7 +62,7 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
// 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 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
@override
RouteInformation restoreRouteInformation(RouteMatchList configuration) {
return RouteInformation(
location: configuration.location.toString(),
location: configuration.uri.toString(),
state: configuration.extra,
);
}

View File

@ -26,13 +26,13 @@ FutureOr<RouteMatchList> redirect(
{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 @@ FutureOr<RouteMatchList> redirect(
);
}
// 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 @@ FutureOr<RouteMatchList> redirect(
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 @@ FutureOr<RouteMatchList> redirect(
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 @@ FutureOr<String?> _getRouteLevelRedirect(
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 @@ class RedirectionError extends Error implements UnsupportedError {
@override
String toString() => '${super.toString()} ${<String>[
...matches.map(
(RouteMatchList routeMatches) => routeMatches.location.toString()),
...matches
.map((RouteMatchList routeMatches) => routeMatches.uri.toString()),
].join(' => ')}';
}

View File

@ -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 @@ class GoRoute extends RouteBase {
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 @@ class GoRoute extends RouteBase {
/// 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;
}

View File

@ -146,8 +146,18 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
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 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
_routerDelegate.navigatorKey.currentContext!,
)
.then<void>((RouteMatchList matches) {
_routerDelegate.push(matches.last);
_routerDelegate.push(matches);
});
}
@ -239,7 +249,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
_routerDelegate.navigatorKey.currentContext!,
)
.then<void>((RouteMatchList matchList) {
routerDelegate.replace(matchList.matches.last);
routerDelegate.replace(matchList);
});
}

View File

@ -14,7 +14,7 @@ import 'misc/errors.dart';
@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 @@ class GoRouterState {
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

View File

@ -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

View File

@ -28,18 +28,18 @@ void main() {
navigatorKey: GlobalKey<NavigatorState>(),
);
final RouteMatchList matches = RouteMatchList(<RouteMatch>[
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,
pageKey: const ValueKey<String>('/'),
),
]);
],
Uri.parse('/'),
const <String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(
@ -75,18 +75,18 @@ void main() {
navigatorKey: GlobalKey<NavigatorState>(),
);
final RouteMatchList matches = RouteMatchList(<RouteMatch>[
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,
pageKey: const ValueKey<String>('/'),
),
]);
],
Uri.parse('/'),
<String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(
@ -117,18 +117,18 @@ void main() {
},
);
final RouteMatchList matches = RouteMatchList(<RouteMatch>[
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,
pageKey: const ValueKey<String>('/'),
),
]);
],
Uri.parse('/'),
<String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(
@ -172,28 +172,25 @@ void main() {
},
);
final RouteMatchList matches = RouteMatchList(<RouteMatch>[
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,
pageKey: const ValueKey<String>(''),
),
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,
pageKey: const ValueKey<String>('/details'),
),
]);
],
Uri.parse('/details'),
<String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(
@ -250,18 +247,18 @@ void main() {
},
);
final RouteMatchList matches = RouteMatchList(<RouteMatch>[
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,
pageKey: const ValueKey<String>('/a/details'),
),
]);
],
Uri.parse('/a/details'),
<String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(

View File

@ -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 @@ void main() {
(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,8 +110,7 @@ void main() {
});
group('replace', () {
testWidgets(
'It should replace the last match with the given one',
testWidgets('It should replace the last match with the given one',
(WidgetTester tester) async {
final GoRouter goRouter = GoRouter(
initialLocation: '/',
@ -148,12 +144,15 @@ void main() {
reason: 'The last match should have been removed',
);
expect(
goRouter.routerDelegate.matches.last.fullpath,
(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 @@ void main() {
expect(goRouter.routerDelegate.matches.matches.length, 1);
expect(
goRouter.routerDelegate.matches.matches[0].pageKey,
null,
isNotNull,
);
goRouter.push('/a');
@ -228,13 +227,7 @@ void main() {
);
expect(
goRouter.routerDelegate.matches.last,
isA<RouteMatch>()
.having(
(RouteMatch match) => match.fullpath,
'match.fullpath',
'/page-1',
)
.having(
isA<RouteMatch>().having(
(RouteMatch match) => (match.route as GoRoute).name,
'match.route.name',
'page1',

View File

@ -42,7 +42,7 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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',

View File

@ -9,7 +9,9 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/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 @@ void main() {
];
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,6 +74,8 @@ void main() {
test('leading / on sub-route', () {
expect(() {
GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: dummy,
@ -81,12 +85,16 @@ void main() {
builder: dummy,
),
],
),
],
);
}, throwsA(isAssertionError));
});
test('trailing / on sub-route', () {
expect(() {
GoRouter(
routes: <RouteBase>[
GoRoute(
path: '/',
builder: dummy,
@ -96,6 +104,8 @@ void main() {
builder: dummy,
),
],
),
],
);
}, throwsA(isAssertionError));
});
@ -328,44 +338,44 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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',

View File

@ -14,47 +14,36 @@ void main() {
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 @@ void main() {
}
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 @@ void main() {
),
],
);
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 @@ void main() {
}
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);
});
});
}

View File

@ -56,9 +56,8 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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 @@ void main() {
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');
});