[go_router] Cleans up route match API and introduces dart fix (#3819)

Clean up API around RouteMatch/RouteMatchList/GoRouterState,

This is a breaking change that renamed some of the GoRouterState property to have a more descriptive name as flutter style guide suggested https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#avoid-abbreviations

also introducing dart fix to help with migration
This commit is contained in:
chunhtai
2023-04-28 18:45:10 -07:00
committed by GitHub
parent 2f95ecda81
commit b01736711c
40 changed files with 966 additions and 448 deletions

View File

@ -1,3 +1,19 @@
## 7.0.0
- **BREAKING CHANGE**:
- For the below changes, run `dart fix --apply` to automatically migrate your code.
- `GoRouteState.subloc` has been renamed to `GoRouteState.matchedLocation`.
- `GoRouteState.params` has been renamed to `GoRouteState.pathParameters`.
- `GoRouteState.fullpath` has been renamed to `GoRouteState.fullPath`.
- `GoRouteState.queryParams` has been renamed to `GoRouteState.queryParameters`.
- `params` and `queryParams` in `GoRouteState.namedLocation` have been renamed to `pathParameters` and `queryParameters`.
- `params` and `queryParams` in `GoRouter`'s `namedLocation`, `pushNamed`, `pushReplacementNamed`
`replaceNamed` have been renamed to `pathParameters` and `queryParameters`.
- For the below changes, please follow the [migration guide](https://docs.google.com/document/d/10Xbpifbs4E-zh6YE5akIO8raJq_m3FIXs6nUGdOspOg).
- `params` and `queryParams` in `BuildContext`'s `namedLocation`, `pushNamed`, `pushReplacementNamed`
`replaceNamed` have been renamed to `pathParameters` and `queryParameters`.
- Cleans up API and makes RouteMatchList immutable.
## 6.5.9
- Removes navigator keys from `GoRouteData` and `ShellRouteData`.

View File

@ -37,6 +37,7 @@ See the API documentation for details on the following topics:
- [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html)
## Migration guides
- [Migrating to 7.0.0](https://docs.google.com/document/d/10Xbpifbs4E-zh6YE5akIO8raJq_m3FIXs6nUGdOspOg).
- [Migrating to 6.0.0](https://flutter.dev/go/go-router-v6-breaking-changes)
- [Migrating to 5.1.2](https://flutter.dev/go/go-router-v5-1-2-breaking-changes)
- [Migrating to 5.0](https://flutter.dev/go/go-router-v5-breaking-changes)

View File

@ -0,0 +1,5 @@
include: ../../analysis_options.yaml
analyzer:
exclude:
- "test_fixes/**"

View File

@ -43,7 +43,7 @@ the builder callback:
```dart
GoRoute(
path: '/users/:userId',
builder: (context, state) => const UserScreen(id: state.params['userId']),
builder: (context, state) => const UserScreen(id: state.pathParameters['userId']),
),
```
@ -55,7 +55,7 @@ after the `?`), use [GoRouterState][]. For example, a URL path such as
```dart
GoRoute(
path: '/users',
builder: (context, state) => const UsersScreen(filter: state.queryParams['filter']),
builder: (context, state) => const UsersScreen(filter: state.queryParameters['filter']),
),
```

View File

@ -14,7 +14,7 @@ To navigate to a route using its name, call [`goNamed`](https://pub.dev/document
```dart
TextButton(
onPressed: () {
context.goNamed('song', params: {'songId': 123});
context.goNamed('song', pathParameters: {'songId': 123});
},
child: const Text('Go to song 2'),
),
@ -25,7 +25,7 @@ Alternatively, you can look up the location for a name using `namedLocation`:
```dart
TextButton(
onPressed: () {
final String location = context.namedLocation('song', params: {'songId': 123});
final String location = context.namedLocation('song', pathParameters: {'songId': 123});
context.go(location);
},
child: const Text('Go to song 2'),

View File

@ -55,7 +55,7 @@ class App extends StatelessWidget {
// cause go_router to reparse current route if StreamAuth has new sign-in
// information.
final bool loggedIn = await StreamAuthScope.of(context).isSignedIn();
final bool loggingIn = state.subloc == '/login';
final bool loggingIn = state.matchedLocation == '/login';
if (!loggedIn) {
return '/login';
}

View File

@ -63,7 +63,7 @@ class Bookstore extends StatelessWidget {
GoRoute(
path: '/book/:bookId',
redirect: (BuildContext context, GoRouterState state) =>
'/books/all/${state.params['bookId']}',
'/books/all/${state.pathParameters['bookId']}',
),
GoRoute(
path: '/books/:kind(new|all|popular)',
@ -72,14 +72,14 @@ class Bookstore extends StatelessWidget {
key: _scaffoldKey,
child: BookstoreScaffold(
selectedTab: ScaffoldTab.books,
child: BooksScreen(state.params['kind']!),
child: BooksScreen(state.pathParameters['kind']!),
),
),
routes: <GoRoute>[
GoRoute(
path: ':bookId',
builder: (BuildContext context, GoRouterState state) {
final String bookId = state.params['bookId']!;
final String bookId = state.pathParameters['bookId']!;
final Book? selectedBook = libraryInstance.allBooks
.firstWhereOrNull((Book b) => b.id.toString() == bookId);
@ -91,7 +91,7 @@ class Bookstore extends StatelessWidget {
GoRoute(
path: '/author/:authorId',
redirect: (BuildContext context, GoRouterState state) =>
'/authors/${state.params['authorId']}',
'/authors/${state.pathParameters['authorId']}',
),
GoRoute(
path: '/authors',
@ -107,7 +107,7 @@ class Bookstore extends StatelessWidget {
GoRoute(
path: ':authorId',
builder: (BuildContext context, GoRouterState state) {
final int authorId = int.parse(state.params['authorId']!);
final int authorId = int.parse(state.pathParameters['authorId']!);
final Author? selectedAuthor = libraryInstance.allAuthors
.firstWhereOrNull((Author a) => a.id == authorId);
@ -135,7 +135,7 @@ class Bookstore extends StatelessWidget {
String? _guard(BuildContext context, GoRouterState state) {
final bool signedIn = _auth.signedIn;
final bool signingIn = state.subloc == '/signin';
final bool signingIn = state.matchedLocation == '/signin';
// Go to /signin if the user is not signed in
if (!signedIn && !signingIn) {

View File

@ -84,14 +84,15 @@ class App extends StatelessWidget {
name: 'family',
path: 'family/:fid',
builder: (BuildContext context, GoRouterState state) =>
FamilyScreen(fid: state.params['fid']!),
FamilyScreen(fid: state.pathParameters['fid']!),
routes: <GoRoute>[
GoRoute(
name: 'person',
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
return PersonScreen(
fid: state.params['fid']!, pid: state.params['pid']!);
fid: state.pathParameters['fid']!,
pid: state.pathParameters['pid']!);
},
),
],
@ -119,7 +120,7 @@ class HomeScreen extends StatelessWidget {
ListTile(
title: Text(entry.value.name),
onTap: () => context.go(context.namedLocation('family',
params: <String, String>{'fid': entry.key})),
pathParameters: <String, String>{'fid': entry.key})),
)
],
),
@ -147,8 +148,8 @@ class FamilyScreen extends StatelessWidget {
title: Text(entry.value.name),
onTap: () => context.go(context.namedLocation(
'person',
params: <String, String>{'fid': fid, 'pid': entry.key},
queryParams: <String, String>{'qid': 'quid'},
pathParameters: <String, String>{'fid': fid, 'pid': entry.key},
queryParameters: <String, String>{'qid': 'quid'},
)),
),
],

View File

@ -108,8 +108,8 @@ class Page1Screen extends StatelessWidget {
ElevatedButton(
onPressed: () => context.goNamed(
'page2',
params: <String, String>{'p1': 'pv1'},
queryParams: <String, String>{'q1': 'qv1'},
pathParameters: <String, String>{'p1': 'pv1'},
queryParameters: <String, String>{'q1': 'qv1'},
),
child: const Text('Go to page 2'),
),
@ -134,7 +134,7 @@ class Page2Screen extends StatelessWidget {
ElevatedButton(
onPressed: () => context.goNamed(
'page3',
params: <String, String>{'p1': 'pv2'},
pathParameters: <String, String>{'p1': 'pv2'},
),
child: const Text('Go to page 3'),
),

View File

@ -32,7 +32,7 @@ class App extends StatelessWidget {
path: '/page2',
builder: (BuildContext context, GoRouterState state) =>
Page2ScreenWithPush(
int.parse(state.queryParams['push-count']!),
int.parse(state.queryParameters['push-count']!),
),
),
],

View File

@ -9,9 +9,9 @@ import 'package:go_router/go_router.dart';
//
// The route segments that start with ':' are treated as path parameters when
// defining GoRoute[s]. The parameter values can be accessed through
// GoRouterState.params.
// GoRouterState.pathParameters.
//
// The query parameters are automatically stored in GoRouterState.queryParams.
// The query parameters are automatically stored in GoRouterState.queryParameters.
/// Family data class.
class Family {
@ -84,8 +84,8 @@ class App extends StatelessWidget {
path: 'family/:fid',
builder: (BuildContext context, GoRouterState state) {
return FamilyScreen(
fid: state.params['fid']!,
asc: state.queryParams['sort'] == 'asc',
fid: state.pathParameters['fid']!,
asc: state.queryParameters['sort'] == 'asc',
);
}),
],
@ -149,7 +149,8 @@ class FamilyScreen extends StatelessWidget {
actions: <Widget>[
IconButton(
onPressed: () => context.goNamed('family',
params: <String, String>{'fid': fid}, queryParams: newQueries),
pathParameters: <String, String>{'fid': fid},
queryParameters: newQueries),
tooltip: 'sort ascending or descending',
icon: const Icon(Icons.sort),
)

View File

@ -75,7 +75,7 @@ class App extends StatelessWidget {
redirect: (BuildContext context, GoRouterState state) {
// if the user is not logged in, they need to login
final bool loggedIn = _loginInfo.loggedIn;
final bool loggingIn = state.subloc == '/login';
final bool loggingIn = state.matchedLocation == '/login';
if (!loggedIn) {
return '/login';
}

View File

@ -0,0 +1,151 @@
# Copyright 2014 The Flutter Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# For details regarding the *Flutter Fix* feature, see
# https://flutter.dev/docs/development/tools/flutter-fix
# Please add new fixes to the top of the file, separated by one blank line
# from other fixes. In a comment, include a link to the PR where the change
# requiring the fix was made.
# Every fix must be tested. See the flutter/packages/flutter/test_fixes/README.md
# file for instructions on testing these data driven fixes.
# For documentation about this file format, see
# https://dart.dev/go/data-driven-fixes
version: 1
transforms:
- title: "Replaces 'params' and 'queryParams' in 'GoRouter.replaceNamed' with `pathParameters` and `queryParameters`"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
method: 'replaceNamed'
inClass: 'GoRouter'
changes:
- kind: 'renameParameter'
oldName: 'params'
newName: 'pathParameters'
- kind: 'renameParameter'
oldName: 'queryParams'
newName: 'queryParameters'
- title: "Replaces 'params' and 'queryParams' in 'GoRouter.pushReplacementNamed' with `pathParameters` and `queryParameters`"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
method: 'pushReplacementNamed'
inClass: 'GoRouter'
changes:
- kind: 'renameParameter'
oldName: 'params'
newName: 'pathParameters'
- kind: 'renameParameter'
oldName: 'queryParams'
newName: 'queryParameters'
- title: "Replaces 'params' and 'queryParams' in 'GoRouter.pushNamed' with `pathParameters` and `queryParameters`"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
method: 'pushNamed'
inClass: 'GoRouter'
changes:
- kind: 'renameParameter'
oldName: 'params'
newName: 'pathParameters'
- kind: 'renameParameter'
oldName: 'queryParams'
newName: 'queryParameters'
- title: "Replaces 'params' and 'queryParams' in 'GoRouter.goNamed' with `pathParameters` and `queryParameters`"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
method: 'goNamed'
inClass: 'GoRouter'
changes:
- kind: 'renameParameter'
oldName: 'params'
newName: 'pathParameters'
- kind: 'renameParameter'
oldName: 'queryParams'
newName: 'queryParameters'
- title: "Replaces 'params' and 'queryParams' in 'GoRouter.namedLocation' with `pathParameters` and `queryParameters`"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
method: 'namedLocation'
inClass: 'GoRouter'
changes:
- kind: 'renameParameter'
oldName: 'params'
newName: 'pathParameters'
- kind: 'renameParameter'
oldName: 'queryParams'
newName: 'queryParameters'
- title: "Replaces 'params' and 'queryParams' in 'GoRouterState.namedLocation' with `pathParameters` and `queryParameters`"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
method: 'namedLocation'
inClass: 'GoRouterState'
changes:
- kind: 'renameParameter'
oldName: 'params'
newName: 'pathParameters'
- kind: 'renameParameter'
oldName: 'queryParams'
newName: 'queryParameters'
- title: "Replaces 'GoRouterState.queryParams' with 'GoRouterState.queryParameters'"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
field: 'queryParams'
inClass: 'GoRouterState'
changes:
- kind: 'rename'
newName: 'queryParameters'
- title: "Replaces 'GoRouterState.fullpath' with 'GoRouterState.fullPath'"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
field: 'fullpath'
inClass: 'GoRouterState'
changes:
- kind: 'rename'
newName: 'fullPath'
- title: "Replaces 'GoRouterState.params' with 'GoRouterState.pathParameters'"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
field: 'params'
inClass: 'GoRouterState'
changes:
- kind: 'rename'
newName: 'pathParameters'
- title: "Replaces 'GoRouterState.subloc' with 'GoRouterState.matchedLocation'"
date: 2023-04-24
bulkApply: true
element:
uris: [ 'go_router.dart' ]
field: 'subloc'
inClass: 'GoRouterState'
changes:
- kind: 'rename'
newName: 'matchedLocation'

View File

@ -6,7 +6,6 @@ import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'delegate.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
@ -254,7 +253,7 @@ class RouteBuilder {
}
/// Helper method that builds a [GoRouterState] object for the given [match]
/// and [params].
/// and [pathParameters].
@visibleForTesting
GoRouterState buildState(RouteMatchList matchList, RouteMatch match) {
final RouteBase route = match.route;
@ -269,13 +268,14 @@ class RouteBuilder {
return GoRouterState(
configuration,
location: effectiveMatchList.uri.toString(),
subloc: match.subloc,
matchedLocation: match.matchedLocation,
name: name,
path: path,
fullpath: effectiveMatchList.fullpath,
params: Map<String, String>.from(effectiveMatchList.pathParameters),
fullPath: effectiveMatchList.fullPath,
pathParameters:
Map<String, String>.from(effectiveMatchList.pathParameters),
error: match.error,
queryParams: effectiveMatchList.uri.queryParameters,
queryParameters: effectiveMatchList.uri.queryParameters,
queryParametersAll: effectiveMatchList.uri.queryParametersAll,
extra: match.extra,
pageKey: match.pageKey,
@ -397,7 +397,10 @@ class RouteBuilder {
return _pageBuilderForAppType!(
key: state.pageKey,
name: state.name ?? state.path,
arguments: <String, String>{...state.params, ...state.queryParams},
arguments: <String, String>{
...state.pathParameters,
...state.queryParameters
},
restorationId: state.pageKey.value,
child: child,
);
@ -444,9 +447,9 @@ class RouteBuilder {
final GoRouterState state = GoRouterState(
configuration,
location: uri.toString(),
subloc: uri.path,
matchedLocation: uri.path,
name: null,
queryParams: uri.queryParameters,
queryParameters: uri.queryParameters,
queryParametersAll: uri.queryParametersAll,
error: Exception(error),
pageKey: const ValueKey<String>('error'),

View File

@ -97,7 +97,7 @@ class RouteConfiguration {
if (route is! GoRoute) {
continue;
}
for (final String pathParam in route.pathParams) {
for (final String pathParam in route.pathParameters) {
if (usedPathParams.containsKey(pathParam)) {
final bool sameRoute = usedPathParams[pathParam] == route;
throw GoError(
@ -106,7 +106,7 @@ class RouteConfiguration {
usedPathParams[pathParam] = route;
}
_debugVerifyNoDuplicatePathParameter(route.routes, usedPathParams);
route.pathParams.forEach(usedPathParams.remove);
route.pathParameters.forEach(usedPathParams.remove);
}
return true;
}
@ -128,14 +128,14 @@ class RouteConfiguration {
/// Looks up the url location by a [GoRoute]'s name.
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
}) {
assert(() {
log.info('getting location for name: '
'"$name"'
'${params.isEmpty ? '' : ', params: $params'}'
'${queryParams.isEmpty ? '' : ', queryParams: $queryParams'}');
'${pathParameters.isEmpty ? '' : ', pathParameters: $pathParameters'}'
'${queryParameters.isEmpty ? '' : ', queryParameters: $queryParameters'}');
return true;
}());
final String keyName = name.toLowerCase();
@ -146,24 +146,24 @@ class RouteConfiguration {
final List<String> paramNames = <String>[];
patternToRegExp(path, paramNames);
for (final String paramName in paramNames) {
assert(params.containsKey(paramName),
assert(pathParameters.containsKey(paramName),
'missing param "$paramName" for $path');
}
// Check that there are no extra params
for (final String key in params.keys) {
for (final String key in pathParameters.keys) {
assert(paramNames.contains(key), 'unknown param "$key" for $path');
}
return true;
}());
final Map<String, String> encodedParams = <String, String>{
for (final MapEntry<String, String> param in params.entries)
for (final MapEntry<String, String> param in pathParameters.entries)
param.key: Uri.encodeComponent(param.value)
};
final String location = patternToPath(path, encodedParams);
return Uri(
path: location,
queryParameters: queryParams.isEmpty ? null : queryParams)
queryParameters: queryParameters.isEmpty ? null : queryParameters)
.toString();
}
@ -196,9 +196,9 @@ class RouteConfiguration {
int depth, StringBuffer sb) {
for (final RouteBase route in routes) {
if (route is GoRoute) {
final String fullpath = concatenatePaths(parentFullpath, route.path);
sb.writeln(' => ${''.padLeft(depth * 2)}$fullpath');
_debugFullPathsFor(route.routes, fullpath, depth + 1, sb);
final String fullPath = concatenatePaths(parentFullpath, route.path);
sb.writeln(' => ${''.padLeft(depth * 2)}$fullPath');
_debugFullPathsFor(route.routes, fullPath, depth + 1, sb);
} else if (route is ShellRoute) {
_debugFullPathsFor(route.routes, parentFullpath, depth, sb);
}

View File

@ -86,15 +86,19 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
RouteMatchList matches, ValueKey<String> pageKey) async {
final ImperativeRouteMatch<T> newPageKeyMatch = ImperativeRouteMatch<T>(
route: matches.last.route,
subloc: matches.last.subloc,
matchedLocation: matches.last.matchedLocation,
extra: matches.last.extra,
error: matches.last.error,
pageKey: pageKey,
matches: matches,
);
_matchList.push(newPageKeyMatch);
return newPageKeyMatch._future;
_matchList = _matchList.push(newPageKeyMatch);
return newPageKeyMatch.future;
}
void _remove(RouteMatch match) {
_matchList = _matchList.remove(match);
}
/// Pushes the given location onto the page stack.
@ -108,7 +112,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
Future<T?> push<T extends Object?>(RouteMatchList matches) async {
assert(matches.last.route is! ShellRoute);
final ValueKey<String> pageKey = _getNewKeyForPath(matches.fullpath);
final ValueKey<String> pageKey = _getNewKeyForPath(matches.fullPath);
final Future<T?> future = _push(matches, pageKey);
notifyListeners();
return future;
@ -155,7 +159,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
if (match is ImperativeRouteMatch) {
match.complete(result);
}
_matchList.remove(match!);
_remove(match!);
notifyListeners();
assert(() {
_debugAssertMatchListNotEmpty();
@ -175,7 +179,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
/// state and not run any page animation.
void pushReplacement(RouteMatchList matches) {
assert(matches.last.route is! ShellRoute);
_matchList.remove(_matchList.last);
_remove(_matchList.last);
push(matches); // [push] will notify the listeners.
}
@ -193,7 +197,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
assert(matches.last.route is! ShellRoute);
final RouteMatch routeMatch = _matchList.last;
final ValueKey<String> pageKey = routeMatch.pageKey;
_matchList.remove(routeMatch);
_remove(routeMatch);
_push(matches, pageKey);
notifyListeners();
}
@ -309,32 +313,3 @@ class _NavigatorStateIterator extends Iterator<NavigatorState> {
return true;
}
}
/// The route match that represent route pushed through [GoRouter.push].
class ImperativeRouteMatch<T> extends RouteMatch {
/// Constructor for [ImperativeRouteMatch].
ImperativeRouteMatch({
required super.route,
required super.subloc,
required super.extra,
required super.error,
required super.pageKey,
required this.matches,
}) : _completer = Completer<T?>();
/// The matches that produces this route match.
final RouteMatchList matches;
/// The completer for the future returned by [GoRouter.push].
final Completer<T?> _completer;
/// Called when the corresponding [Route] associated with this route match is
/// completed.
void complete([dynamic value]) {
_completer.complete(value as T?);
}
/// The future of the [RouteMatch] completer.
/// When the future completes, this will return the value passed to [complete].
Future<T?> get _future => _completer.future;
}

View File

@ -2,35 +2,44 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'matching.dart';
import 'path_utils.dart';
import 'route.dart';
/// An instance of a GoRoute plus information about the current location.
/// An matched result by matching a [RouteBase] against a location.
///
/// This is typically created by calling [RouteMatch.match].
@immutable
class RouteMatch {
/// Constructor for [RouteMatch].
RouteMatch({
const RouteMatch({
required this.route,
required this.subloc,
required this.matchedLocation,
required this.extra,
required this.error,
required this.pageKey,
});
// ignore: public_member_api_docs
/// Generate a [RouteMatch] object by matching the `route` with
/// `remainingLocation`.
///
/// The extracted path parameters, as the result of the matching, are stored
/// into `pathParameters`.
static RouteMatch? match({
required RouteBase route,
required String restLoc, // e.g. person/p1
required String parentSubloc, // e.g. /family/f2
required String remainingLocation, // e.g. person/p1
required String matchedLocation, // e.g. /family/f2
required Map<String, String> pathParameters,
required Object? extra,
}) {
if (route is ShellRoute) {
return RouteMatch(
route: route,
subloc: restLoc,
matchedLocation: remainingLocation,
extra: extra,
error: null,
pageKey: ValueKey<String>(route.hashCode.toString()),
@ -38,7 +47,7 @@ class RouteMatch {
} else if (route is GoRoute) {
assert(!route.path.contains('//'));
final RegExpMatch? match = route.matchPatternAsPrefix(restLoc);
final RegExpMatch? match = route.matchPatternAsPrefix(remainingLocation);
if (match == null) {
return null;
}
@ -48,23 +57,31 @@ class RouteMatch {
pathParameters[param.key] = Uri.decodeComponent(param.value);
}
final String pathLoc = patternToPath(route.path, encodedParams);
final String subloc = concatenatePaths(parentSubloc, pathLoc);
final String newMatchedLocation =
concatenatePaths(matchedLocation, pathLoc);
return RouteMatch(
route: route,
subloc: subloc,
matchedLocation: newMatchedLocation,
extra: extra,
error: null,
pageKey: ValueKey<String>(route.hashCode.toString()),
);
}
throw MatcherError('Unexpected route type: $route', restLoc);
throw MatcherError('Unexpected route type: $route', remainingLocation);
}
/// The matched route.
final RouteBase route;
/// The matched location.
final String subloc; // e.g. /family/f2
/// The location string that matches the [route].
///
/// for example:
///
/// uri = '/family/f2/person/p2'
/// route = GoRoute('/family/:id)
///
/// matchedLocation = '/family/f2'
final String matchedLocation;
/// An extra object to pass along with the navigation.
final Object? extra;
@ -74,4 +91,59 @@ class RouteMatch {
/// Value key of type string, to hold a unique reference to a page.
final ValueKey<String> pageKey;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is RouteMatch &&
route == other.route &&
matchedLocation == other.matchedLocation &&
extra == other.extra &&
pageKey == other.pageKey;
}
@override
int get hashCode => Object.hash(route, matchedLocation, extra, pageKey);
}
/// The route match that represent route pushed through [GoRouter.push].
class ImperativeRouteMatch<T> extends RouteMatch {
/// Constructor for [ImperativeRouteMatch].
ImperativeRouteMatch({
required super.route,
required super.matchedLocation,
required super.extra,
required super.error,
required super.pageKey,
required this.matches,
}) : _completer = Completer<T?>();
/// The matches that produces this route match.
final RouteMatchList matches;
/// The completer for the future returned by [GoRouter.push].
final Completer<T?> _completer;
/// Called when the corresponding [Route] associated with this route match is
/// completed.
void complete([dynamic value]) {
_completer.complete(value as T?);
}
/// The future of the [RouteMatch] completer.
/// When the future completes, this will return the value passed to [complete].
Future<T?> get future => _completer.future;
// An ImperativeRouteMatch has its own life cycle due the the _completer.
// comparing _completer between instances would be the same thing as
// comparing object reference.
@override
bool operator ==(Object other) {
return identical(this, other);
}
@override
int get hashCode => identityHashCode(this);
}

View File

@ -2,11 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'delegate.dart';
import 'match.dart';
import 'path_utils.dart';
@ -25,16 +25,17 @@ class RouteMatcher {
final Map<String, String> pathParameters = <String, String>{};
final List<RouteMatch> matches =
_getLocRouteMatches(uri, extra, pathParameters);
return RouteMatchList(matches, uri, pathParameters);
return RouteMatchList(
matches: matches, uri: uri, pathParameters: pathParameters);
}
List<RouteMatch> _getLocRouteMatches(
Uri uri, Object? extra, Map<String, String> pathParameters) {
final List<RouteMatch>? result = _getLocRouteRecursively(
loc: uri.path,
restLoc: uri.path,
location: uri.path,
remainingLocation: uri.path,
routes: configuration.routes,
parentSubloc: '',
matchedLocation: '',
pathParameters: pathParameters,
extra: extra,
);
@ -50,19 +51,50 @@ class RouteMatcher {
/// The list of [RouteMatch] objects.
///
/// This corresponds to the GoRouter's history.
@immutable
class RouteMatchList {
/// RouteMatchList constructor.
RouteMatchList(List<RouteMatch> matches, this._uri, this.pathParameters)
: _matches = matches,
fullpath = _generateFullPath(matches);
RouteMatchList({
required this.matches,
required this.uri,
required this.pathParameters,
}) : fullPath = _generateFullPath(matches);
/// Constructs an empty matches object.
static RouteMatchList empty =
RouteMatchList(<RouteMatch>[], Uri.parse(''), const <String, String>{});
static RouteMatchList empty = RouteMatchList(
matches: const <RouteMatch>[],
uri: Uri(),
pathParameters: const <String, String>{});
/// The route matches.
final List<RouteMatch> matches;
/// Parameters for the matched route, URI-encoded.
///
/// The parameters only reflects [RouteMatch]s that are not
/// [ImperativeRouteMatch].
final Map<String, String> pathParameters;
/// The uri of the current match.
///
/// This uri only reflects [RouteMatch]s that are not [ImperativeRouteMatch].
final Uri uri;
/// the full path pattern that matches the uri.
///
/// For example:
///
/// ```dart
/// '/family/:fid/person/:pid'
/// ```
final String fullPath;
/// Generates the full path (ex: `'/family/:fid/person/:pid'`) of a list of
/// [RouteMatch].
///
/// This method ignores [ImperativeRouteMatch]s in the `matches`, as they
/// don't contribute to the path.
///
/// This methods considers that [matches]'s elements verify the go route
/// structure given to `GoRouter`. For example, if the routes structure is
///
@ -90,7 +122,8 @@ class RouteMatchList {
static String _generateFullPath(Iterable<RouteMatch> matches) {
final StringBuffer buffer = StringBuffer();
bool addsSlash = false;
for (final RouteMatch match in matches) {
for (final RouteMatch match in matches
.where((RouteMatch match) => match is! ImperativeRouteMatch)) {
final RouteBase route = match.route;
if (route is GoRoute) {
if (addsSlash) {
@ -103,66 +136,61 @@ class RouteMatchList {
return buffer.toString();
}
final List<RouteMatch> _matches;
/// the full path pattern that matches the uri.
///
/// For example:
///
/// ```dart
/// '/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.
Uri get uri => _uri;
Uri _uri;
/// Returns true if there are no matches.
bool get isEmpty => _matches.isEmpty;
bool get isEmpty => matches.isEmpty;
/// Returns true if there are matches.
bool get isNotEmpty => _matches.isNotEmpty;
bool get isNotEmpty => matches.isNotEmpty;
/// Pushes a match onto the list of matches.
void push(RouteMatch match) {
_matches.add(match);
/// Returns a new instance of RouteMatchList with the input `match` pushed
/// onto the current instance.
RouteMatchList push<T>(ImperativeRouteMatch<T> match) {
// Imperative route match doesn't change the uri and path parameters.
return _copyWith(matches: <RouteMatch>[...matches, match]);
}
/// Removes the match from the list.
void remove(RouteMatch match) {
final int index = _matches.indexOf(match);
/// Returns a new instance of RouteMatchList with the input `match` removed
/// from the current instance.
RouteMatchList remove(RouteMatch match) {
final List<RouteMatch> newMatches = matches.toList();
final int index = newMatches.indexOf(match);
assert(index != -1);
_matches.removeRange(index, _matches.length);
newMatches.removeRange(index, newMatches.length);
// Also pop ShellRoutes when there are no subsequent route matches
while (_matches.isNotEmpty && _matches.last.route is ShellRoute) {
_matches.removeLast();
while (newMatches.isNotEmpty && newMatches.last.route is ShellRoute) {
newMatches.removeLast();
}
// Removing ImperativeRouteMatch should not change uri and pathParameters.
if (match is ImperativeRouteMatch) {
return _copyWith(matches: newMatches);
}
final String fullPath = _generateFullPath(
_matches.where((RouteMatch match) => match is! ImperativeRouteMatch));
newMatches.where((RouteMatch match) => match is! ImperativeRouteMatch));
// Need to remove path parameters that are no longer in the fullPath.
final List<String> newParameters = <String>[];
patternToRegExp(fullPath, newParameters);
final Set<String> validParameters = newParameters.toSet();
pathParameters.removeWhere(
(String key, String value) => !validParameters.contains(key));
_uri = _uri.replace(path: patternToPath(fullPath, pathParameters));
final Map<String, String> newPathParameters =
Map<String, String>.fromEntries(
pathParameters.entries.where((MapEntry<String, String> value) =>
validParameters.contains(value.key)),
);
final Uri newUri =
uri.replace(path: patternToPath(fullPath, newPathParameters));
return _copyWith(
matches: newMatches,
uri: newUri,
pathParameters: newPathParameters,
);
}
/// An optional object provided by the app during navigation.
Object? get extra => _matches.isEmpty ? null : _matches.last.extra;
Object? get extra => matches.isEmpty ? null : matches.last.extra;
/// The last matching route.
RouteMatch get last => _matches.last;
/// The route matches.
List<RouteMatch> get matches => _matches;
RouteMatch get last => matches.last;
/// Returns true if the current match intends to display an error screen.
bool get isError => matches.length == 1 && matches.first.error != null;
@ -170,9 +198,44 @@ class RouteMatchList {
/// Returns the error that this match intends to display.
Exception? get error => matches.first.error;
RouteMatchList _copyWith({
List<RouteMatch>? matches,
Uri? uri,
Map<String, String>? pathParameters,
}) {
return RouteMatchList(
matches: matches ?? this.matches,
uri: uri ?? this.uri,
pathParameters: pathParameters ?? this.pathParameters);
}
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is RouteMatchList &&
const ListEquality<RouteMatch>().equals(matches, other.matches) &&
uri == other.uri &&
const MapEquality<String, String>()
.equals(pathParameters, other.pathParameters);
}
@override
int get hashCode {
return Object.hash(
Object.hashAll(matches),
uri,
Object.hashAllUnordered(
pathParameters.entries.map<int>((MapEntry<String, String> entry) =>
Object.hash(entry.key, entry.value)),
),
);
}
@override
String toString() {
return '${objectRuntimeType(this, 'RouteMatchList')}($fullpath)';
return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)';
}
}
@ -198,17 +261,17 @@ class MatcherError extends Error {
/// For example, for a given `loc` `/a/b/c/d`, this function will return the
/// list of [RouteBase] `[GoRouteA(), GoRouterB(), GoRouteC(), GoRouterD()]`.
///
/// - [loc] is the complete URL to match (without the query parameters). For
/// example, for the URL `/a/b?c=0`, [loc] will be `/a/b`.
/// - [restLoc] is the remaining part of the URL to match while [parentSubloc]
/// - [location] is the complete URL to match (without the query parameters). For
/// example, for the URL `/a/b?c=0`, [location] will be `/a/b`.
/// - [remainingLocation] is the remaining part of the URL to match while [matchedLocation]
/// is the part of the URL that has already been matched. For examples, for
/// the URL `/a/b/c/d`, at some point, [restLoc] would be `/c/d` and
/// [parentSubloc] will be `/a/b`.
/// - [routes] are the possible [RouteBase] to match to [restLoc].
/// the URL `/a/b/c/d`, at some point, [remainingLocation] would be `/c/d` and
/// [matchedLocation] will be `/a/b`.
/// - [routes] are the possible [RouteBase] to match to [remainingLocation].
List<RouteMatch>? _getLocRouteRecursively({
required String loc,
required String restLoc,
required String parentSubloc,
required String location,
required String remainingLocation,
required String matchedLocation,
required List<RouteBase> routes,
required Map<String, String> pathParameters,
required Object? extra,
@ -221,8 +284,8 @@ List<RouteMatch>? _getLocRouteRecursively({
final RouteMatch? match = RouteMatch.match(
route: route,
restLoc: restLoc,
parentSubloc: parentSubloc,
remainingLocation: remainingLocation,
matchedLocation: matchedLocation,
pathParameters: subPathParameters,
extra: extra,
);
@ -232,9 +295,9 @@ List<RouteMatch>? _getLocRouteRecursively({
}
if (match.route is GoRoute &&
match.subloc.toLowerCase() == loc.toLowerCase()) {
match.matchedLocation.toLowerCase() == location.toLowerCase()) {
// If it is a complete match, then return the matched route
// NOTE: need a lower case match because subloc is canonicalized to match
// NOTE: need a lower case match because matchedLocation is canonicalized to match
// the path case whereas the location can be of any case and still match
result = <RouteMatch>[match];
} else if (route.routes.isEmpty) {
@ -245,21 +308,21 @@ List<RouteMatch>? _getLocRouteRecursively({
final String childRestLoc;
final String newParentSubLoc;
if (match.route is ShellRoute) {
childRestLoc = restLoc;
newParentSubLoc = parentSubloc;
childRestLoc = remainingLocation;
newParentSubLoc = matchedLocation;
} else {
assert(loc.startsWith(match.subloc));
assert(restLoc.isNotEmpty);
assert(location.startsWith(match.matchedLocation));
assert(remainingLocation.isNotEmpty);
childRestLoc =
loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1));
newParentSubLoc = match.subloc;
childRestLoc = location.substring(match.matchedLocation.length +
(match.matchedLocation == '/' ? 0 : 1));
newParentSubLoc = match.matchedLocation;
}
final List<RouteMatch>? subRouteMatch = _getLocRouteRecursively(
loc: loc,
restLoc: childRestLoc,
parentSubloc: newParentSubLoc,
location: location,
remainingLocation: childRestLoc,
matchedLocation: newParentSubLoc,
routes: route.routes,
pathParameters: subPathParameters,
extra: extra,
@ -284,20 +347,21 @@ List<RouteMatch>? _getLocRouteRecursively({
RouteMatchList errorScreen(Uri uri, String errorMessage) {
final Exception error = Exception(errorMessage);
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'),
matches: <RouteMatch>[
RouteMatch(
matchedLocation: uri.path,
extra: null,
error: error,
route: GoRoute(
path: uri.toString(),
pageBuilder: (BuildContext context, GoRouterState state) {
throw UnimplementedError();
},
),
],
uri,
const <String, String>{});
pageKey: const ValueKey<String>('error'),
),
],
uri: uri,
pathParameters: const <String, String>{},
);
}

View File

@ -12,11 +12,11 @@ extension GoRouterHelper on BuildContext {
/// Get a location from route name and parameters.
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
}) =>
GoRouter.of(this)
.namedLocation(name, params: params, queryParams: queryParams);
GoRouter.of(this).namedLocation(name,
pathParameters: pathParameters, queryParameters: queryParameters);
/// Navigate to a location.
void go(String location, {Object? extra}) =>
@ -25,14 +25,14 @@ extension GoRouterHelper on BuildContext {
/// Navigate to a named route.
void goNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) =>
GoRouter.of(this).goNamed(
name,
params: params,
queryParams: queryParams,
pathParameters: pathParameters,
queryParameters: queryParameters,
extra: extra,
);
@ -50,14 +50,14 @@ extension GoRouterHelper on BuildContext {
/// Navigate to a named route onto the page stack.
Future<T?> pushNamed<T extends Object?>(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) =>
GoRouter.of(this).pushNamed<T>(
name,
params: params,
queryParams: queryParams,
pathParameters: pathParameters,
queryParameters: queryParameters,
extra: extra,
);
@ -81,7 +81,7 @@ extension GoRouterHelper on BuildContext {
GoRouter.of(this).pushReplacement(location, extra: extra);
/// Replaces the top-most page of the page stack with the named route w/
/// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid':
/// optional parameters, e.g. `name='person', pathParameters={'fid': 'f2', 'pid':
/// 'p1'}`.
///
/// See also:
@ -89,14 +89,14 @@ extension GoRouterHelper on BuildContext {
/// * [pushNamed] which pushes a named route onto the page stack.
void pushReplacementNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) =>
GoRouter.of(this).pushReplacementNamed(
name,
params: params,
queryParams: queryParams,
pathParameters: pathParameters,
queryParameters: queryParameters,
extra: extra,
);
@ -117,8 +117,8 @@ extension GoRouterHelper on BuildContext {
/// preserving the page key.
///
/// This will preserve the state and not run any page animation. Optional
/// parameters can be providded to the named route, e.g. `name='person',
/// params={'fid': 'f2', 'pid': 'p1'}`.
/// parameters can be provided to the named route, e.g. `name='person',
/// pathParameters={'fid': 'f2', 'pid': 'p1'}`.
///
/// See also:
/// * [pushNamed] which pushes the given location onto the page stack.
@ -126,8 +126,8 @@ extension GoRouterHelper on BuildContext {
/// stack but always uses a new page key.
void replaceNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) =>
GoRouter.of(this).replaceNamed(name, extra: extra);

View File

@ -8,7 +8,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'configuration.dart';
import 'delegate.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
@ -72,12 +71,12 @@ 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(
<RouteMatch>[],
matches: const <RouteMatch>[],
// TODO(chunhtai): remove this ignore and migrate the code
// https://github.com/flutter/flutter/issues/124045.
// ignore: deprecated_member_use, unnecessary_non_null_assertion
Uri.parse(canonicalUri(routeInformation.location!)),
const <String, String>{},
uri: Uri.parse(canonicalUri(routeInformation.location!)),
pathParameters: const <String, String>{},
);
}
Future<RouteMatchList> processRedirectorResult(RouteMatchList matches) {

View File

@ -96,8 +96,8 @@ 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: prevMatchList.uri.path,
queryParams: prevMatchList.uri.queryParameters,
matchedLocation: prevMatchList.uri.path,
queryParameters: prevMatchList.uri.queryParameters,
queryParametersAll: prevMatchList.uri.queryParametersAll,
extra: extra,
pageKey: const ValueKey<String>('topLevel'),
@ -138,13 +138,13 @@ FutureOr<String?> _getRouteLevelRedirect(
GoRouterState(
configuration,
location: matchList.uri.toString(),
subloc: match.subloc,
matchedLocation: match.matchedLocation,
name: route.name,
path: route.path,
fullpath: matchList.fullpath,
fullPath: matchList.fullPath,
extra: match.extra,
params: matchList.pathParameters,
queryParams: matchList.uri.queryParameters,
pathParameters: matchList.pathParameters,
queryParameters: matchList.uri.queryParameters,
queryParametersAll: matchList.uri.queryParametersAll,
pageKey: match.pageKey,
),

View File

@ -42,7 +42,7 @@ import 'typedefs.dart';
/// GoRoute(
/// path: 'family/:fid',
/// pageBuilder: (BuildContext context, GoRouterState state) {
/// final Family family = Families.family(state.params['fid']!);
/// final Family family = Families.family(state.pathParameters['fid']!);
/// return MaterialPage<void>(
/// key: state.pageKey,
/// child: FamilyPage(family: family),
@ -52,8 +52,8 @@ import 'typedefs.dart';
/// GoRoute(
/// path: 'person/:pid',
/// pageBuilder: (BuildContext context, GoRouterState state) {
/// final Family family = Families.family(state.params['fid']!);
/// final Person person = family.person(state.params['pid']!);
/// final Family family = Families.family(state.pathParameters['fid']!);
/// final Person person = family.person(state.pathParameters['pid']!);
/// return MaterialPage<void>(
/// key: state.pageKey,
/// child: PersonPage(family: family, person: person),
@ -137,7 +137,7 @@ class GoRoute extends RouteBase {
'builder, pageBuilder, or redirect must be provided'),
super._() {
// cache the path regexp and parameters
_pathRE = patternToRegExp(path, pathParams);
_pathRE = patternToRegExp(path, pathParameters);
}
/// Optional name of the route.
@ -169,8 +169,8 @@ class GoRoute extends RouteBase {
///
/// context.go(
/// context.namedLocation('family'),
/// params: <String, String>{'fid': 123},
/// queryParams: <String, String>{'qid': 'quid'},
/// pathParameters: <String, String>{'fid': 123},
/// queryParameters: <String, String>{'qid': 'quid'},
/// );
/// ```
///
@ -228,7 +228,7 @@ class GoRoute extends RouteBase {
/// path: '/',
/// builder: (BuildContext context, GoRouterState state) => FamilyPage(
/// families: Families.family(
/// state.params['id'],
/// state.pathParameters['id'],
/// ),
/// ),
/// ),
@ -306,11 +306,11 @@ class GoRoute extends RouteBase {
/// Extract the path parameters from a match.
Map<String, String> extractPathParams(RegExpMatch match) =>
extractPathParameters(pathParams, match);
extractPathParameters(pathParameters, match);
/// The path parameters in this route.
@internal
final List<String> pathParams = <String>[];
final List<String> pathParameters = <String>[];
@override
String toString() {

View File

@ -8,6 +8,7 @@ import 'configuration.dart';
import 'delegate.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
import 'matching.dart';
import 'misc/inherited_router.dart';
import 'parser.dart';
@ -179,13 +180,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// This is useful for redirecting to a named location.
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
}) =>
_routeInformationParser.configuration.namedLocation(
name,
params: params,
queryParams: queryParams,
pathParameters: pathParameters,
queryParameters: queryParameters,
);
/// Navigate to a URI location w/ optional query parameters, e.g.
@ -203,16 +204,17 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
}
/// Navigate to a named route w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
/// `name='person', pathParameters={'fid': 'f2', 'pid': 'p1'}`
/// Navigate to the named route.
void goNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) =>
go(
namedLocation(name, params: params, queryParams: queryParams),
namedLocation(name,
pathParameters: pathParameters, queryParameters: queryParameters),
extra: extra,
);
@ -245,15 +247,16 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
}
/// Push a named route onto the page stack w/ optional parameters, e.g.
/// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
/// `name='person', pathParameters={'fid': 'f2', 'pid': 'p1'}`
Future<T?> pushNamed<T extends Object?>(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) =>
push<T>(
namedLocation(name, params: params, queryParams: queryParams),
namedLocation(name,
pathParameters: pathParameters, queryParameters: queryParameters),
extra: extra,
);
@ -283,7 +286,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
}
/// Replaces the top-most page of the page stack with the named route w/
/// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid':
/// optional parameters, e.g. `name='person', pathParameters={'fid': 'f2', 'pid':
/// 'p1'}`.
///
/// See also:
@ -291,12 +294,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// * [pushNamed] which pushes a named route onto the page stack.
void pushReplacementNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) {
pushReplacement(
namedLocation(name, params: params, queryParams: queryParams),
namedLocation(name,
pathParameters: pathParameters, queryParameters: queryParameters),
extra: extra,
);
}
@ -332,7 +336,7 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
///
/// This will preserve the state and not run any page animation. Optional
/// parameters can be providded to the named route, e.g. `name='person',
/// params={'fid': 'f2', 'pid': 'p1'}`.
/// pathParameters={'fid': 'f2', 'pid': 'p1'}`.
///
/// See also:
/// * [pushNamed] which pushes the given location onto the page stack.
@ -340,12 +344,13 @@ class GoRouter extends ChangeNotifier implements RouterConfig<RouteMatchList> {
/// stack but always uses a new page key.
void replaceNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) {
replace(
namedLocation(name, params: params, queryParams: queryParams),
namedLocation(name,
pathParameters: pathParameters, queryParameters: queryParameters),
extra: extra,
);
}

View File

@ -16,12 +16,12 @@ class GoRouterState {
const GoRouterState(
this._configuration, {
required this.location,
required this.subloc,
required this.matchedLocation,
required this.name,
this.path,
this.fullpath,
this.params = const <String, String>{},
this.queryParams = const <String, String>{},
this.fullPath,
this.pathParameters = const <String, String>{},
this.queryParameters = const <String, String>{},
this.queryParametersAll = const <String, List<String>>{},
this.extra,
this.error,
@ -35,8 +35,15 @@ class GoRouterState {
/// The full location of the route, e.g. /family/f2/person/p1
final String location;
/// The location of this sub-route, e.g. /family/f2
final String subloc;
/// The matched location until this point.
///
/// For example:
///
/// location = /family/f2/person/p1
/// route = GoRoute('/family/:id')
///
/// matchedLocation = /family/f2
final String matchedLocation;
/// The optional name of the route.
final String? name;
@ -45,13 +52,13 @@ class GoRouterState {
final String? path;
/// The full path to this sub-route, e.g. /family/:fid
final String? fullpath;
final String? fullPath;
/// The parameters for this sub-route, e.g. {'fid': 'f2'}
final Map<String, String> params;
final Map<String, String> pathParameters;
/// The query parameters for the location, e.g. {'from': '/family/f2'}
final Map<String, String> queryParams;
final Map<String, String> queryParameters;
/// The query parameters for the location,
/// e.g. `{'q1': ['v1'], 'q2': ['v2', 'v3']}`
@ -98,7 +105,7 @@ class GoRouterState {
/// class MyWidget extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Text('${GoRouterState.of(context).params['id']}');
/// return Text('${GoRouterState.of(context).pathParameters['id']}');
/// }
/// }
/// ```
@ -125,26 +132,27 @@ class GoRouterState {
/// Get a location from route name and parameters.
/// This is useful for redirecting to a named location.
@Deprecated('Use GoRouter.of(context).namedLocation instead')
// TODO(chunhtai): remove this method when go_router can provide a way to
// look up named location during redirect.
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, String> queryParams = const <String, String>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, String> queryParameters = const <String, String>{},
}) {
return _configuration.namedLocation(name,
params: params, queryParams: queryParams);
pathParameters: pathParameters, queryParameters: queryParameters);
}
@override
bool operator ==(Object other) {
return other is GoRouterState &&
other.location == location &&
other.subloc == subloc &&
other.matchedLocation == matchedLocation &&
other.name == name &&
other.path == path &&
other.fullpath == fullpath &&
other.params == params &&
other.queryParams == queryParams &&
other.fullPath == fullPath &&
other.pathParameters == pathParameters &&
other.queryParameters == queryParameters &&
other.queryParametersAll == queryParametersAll &&
other.extra == extra &&
other.error == error &&
@ -152,8 +160,18 @@ class GoRouterState {
}
@override
int get hashCode => Object.hash(location, subloc, name, path, fullpath,
params, queryParams, queryParametersAll, extra, error, pageKey);
int get hashCode => Object.hash(
location,
matchedLocation,
name,
path,
fullPath,
pathParameters,
queryParameters,
queryParametersAll,
extra,
error,
pageKey);
}
/// An inherited widget to host a [GoRouterStateRegistry] for the subtree.

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: 6.5.9
version: 7.0.0
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
@ -21,3 +21,4 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
path: ^1.8.2

View File

@ -29,17 +29,17 @@ void main() {
);
final RouteMatchList matches = RouteMatchList(
<RouteMatch>[
matches: <RouteMatch>[
RouteMatch(
route: config.routes.first as GoRoute,
subloc: '/',
matchedLocation: '/',
extra: null,
error: null,
pageKey: const ValueKey<String>('/'),
),
],
Uri.parse('/'),
const <String, String>{});
uri: Uri.parse('/'),
pathParameters: const <String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(
@ -76,17 +76,17 @@ void main() {
);
final RouteMatchList matches = RouteMatchList(
<RouteMatch>[
matches: <RouteMatch>[
RouteMatch(
route: config.routes.first,
subloc: '/',
matchedLocation: '/',
extra: null,
error: null,
pageKey: const ValueKey<String>('/'),
),
],
Uri.parse('/'),
<String, String>{});
uri: Uri.parse('/'),
pathParameters: const <String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(
@ -118,17 +118,17 @@ void main() {
);
final RouteMatchList matches = RouteMatchList(
<RouteMatch>[
matches: <RouteMatch>[
RouteMatch(
route: config.routes.first as GoRoute,
subloc: '/',
matchedLocation: '/',
extra: null,
error: null,
pageKey: const ValueKey<String>('/'),
),
],
Uri.parse('/'),
<String, String>{});
uri: Uri.parse('/'),
pathParameters: const <String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(
@ -173,24 +173,24 @@ void main() {
);
final RouteMatchList matches = RouteMatchList(
<RouteMatch>[
matches: <RouteMatch>[
RouteMatch(
route: config.routes.first,
subloc: '',
matchedLocation: '',
extra: null,
error: null,
pageKey: const ValueKey<String>(''),
),
RouteMatch(
route: config.routes.first.routes.first,
subloc: '/details',
matchedLocation: '/details',
extra: null,
error: null,
pageKey: const ValueKey<String>('/details'),
),
],
Uri.parse('/details'),
<String, String>{});
uri: Uri.parse('/details'),
pathParameters: const <String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(
@ -248,17 +248,17 @@ void main() {
);
final RouteMatchList matches = RouteMatchList(
<RouteMatch>[
matches: <RouteMatch>[
RouteMatch(
route: config.routes.first.routes.first as GoRoute,
subloc: '/a/details',
matchedLocation: '/a/details',
extra: null,
error: null,
pageKey: const ValueKey<String>('/a/details'),
),
],
Uri.parse('/a/details'),
<String, String>{});
uri: Uri.parse('/a/details'),
pathParameters: const <String, String>{});
await tester.pumpWidget(
_BuilderTestWidget(

View File

@ -5,7 +5,6 @@
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';

View File

@ -17,13 +17,13 @@ void main() {
path: '/',
builder: (BuildContext context, _) {
final GoRouterState state = GoRouterState.of(context);
return Text('/ ${state.queryParams['p']}');
return Text('/ ${state.queryParameters['p']}');
}),
GoRoute(
path: '/a',
builder: (BuildContext context, _) {
final GoRouterState state = GoRouterState.of(context);
return Text('/a ${state.queryParams['p']}');
return Text('/a ${state.queryParameters['p']}');
}),
];
final GoRouter router = await createRouter(routes, tester);
@ -83,7 +83,7 @@ void main() {
builder: (_, __) {
return Builder(builder: (BuildContext context) {
return Text(
'2 ${GoRouterState.of(context).params['id']}');
'2 ${GoRouterState.of(context).pathParameters['id']}');
});
}),
]),

View File

@ -11,7 +11,6 @@ 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';
@ -152,7 +151,7 @@ void main() {
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/login');
expect(matches.first.matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
});
@ -181,7 +180,7 @@ void main() {
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/login');
expect(matches.first.matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
});
@ -205,7 +204,7 @@ void main() {
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/login');
expect(matches.first.matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
});
@ -224,7 +223,7 @@ void main() {
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/profile/foo');
expect(matches.first.matchedLocation, '/profile/foo');
expect(find.byType(DummyScreen), findsOneWidget);
});
@ -243,7 +242,7 @@ void main() {
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches, hasLength(1));
expect(matches.first.subloc, '/profile/foo');
expect(matches.first.matchedLocation, '/profile/foo');
expect(find.byType(DummyScreen), findsOneWidget);
});
@ -355,9 +354,9 @@ void main() {
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
expect(matches.length, 2);
expect(matches.first.subloc, '/');
expect(matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(matches[1].subloc, '/login');
expect(matches[1].matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
});
@ -402,9 +401,9 @@ void main() {
{
final RouteMatchList matches = router.routerDelegate.matches;
expect(matches.matches.length, 2);
expect(matches.matches.first.subloc, '/');
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(matches.matches[1].subloc, '/login');
expect(matches.matches[1].matchedLocation, '/login');
expect(find.byType(LoginScreen), findsOneWidget);
}
@ -413,9 +412,9 @@ void main() {
{
final RouteMatchList matches = router.routerDelegate.matches;
expect(matches.matches.length, 2);
expect(matches.matches.first.subloc, '/');
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(matches.matches[1].subloc, '/family/f2');
expect(matches.matches[1].matchedLocation, '/family/f2');
expect(find.byType(FamilyScreen), findsOneWidget);
}
@ -424,11 +423,11 @@ void main() {
{
final RouteMatchList matches = router.routerDelegate.matches;
expect(matches.matches.length, 3);
expect(matches.matches.first.subloc, '/');
expect(matches.matches.first.matchedLocation, '/');
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
expect(matches.matches[1].subloc, '/family/f2');
expect(matches.matches[1].matchedLocation, '/family/f2');
expect(find.byType(FamilyScreen, skipOffstage: false), findsOneWidget);
expect(matches.matches[2].subloc, '/family/f2/person/p1');
expect(matches.matches[2].matchedLocation, '/family/f2/person/p1');
expect(find.byType(PersonScreen), findsOneWidget);
}
});
@ -494,11 +493,11 @@ void main() {
path: '/',
builder: (BuildContext context, GoRouterState state) {
expect(state.location, '/');
expect(state.subloc, '/');
expect(state.matchedLocation, '/');
expect(state.name, 'home');
expect(state.path, '/');
expect(state.fullpath, '/');
expect(state.params, <String, String>{});
expect(state.fullPath, '/');
expect(state.pathParameters, <String, String>{});
expect(state.error, null);
if (state.extra != null) {
expect(state.extra! as int, 1);
@ -511,11 +510,11 @@ void main() {
path: 'login',
builder: (BuildContext context, GoRouterState state) {
expect(state.location, '/login');
expect(state.subloc, '/login');
expect(state.matchedLocation, '/login');
expect(state.name, 'login');
expect(state.path, 'login');
expect(state.fullpath, '/login');
expect(state.params, <String, String>{});
expect(state.fullPath, '/login');
expect(state.pathParameters, <String, String>{});
expect(state.error, null);
expect(state.extra! as int, 2);
return const LoginScreen();
@ -529,14 +528,14 @@ void main() {
state.location,
anyOf(<String>['/family/f2', '/family/f2/person/p1']),
);
expect(state.subloc, '/family/f2');
expect(state.matchedLocation, '/family/f2');
expect(state.name, 'family');
expect(state.path, 'family/:fid');
expect(state.fullpath, '/family/:fid');
expect(state.params, <String, String>{'fid': 'f2'});
expect(state.fullPath, '/family/:fid');
expect(state.pathParameters, <String, String>{'fid': 'f2'});
expect(state.error, null);
expect(state.extra! as int, 3);
return FamilyScreen(state.params['fid']!);
return FamilyScreen(state.pathParameters['fid']!);
},
routes: <GoRoute>[
GoRoute(
@ -544,18 +543,18 @@ void main() {
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
expect(state.location, '/family/f2/person/p1');
expect(state.subloc, '/family/f2/person/p1');
expect(state.matchedLocation, '/family/f2/person/p1');
expect(state.name, 'person');
expect(state.path, 'person/:pid');
expect(state.fullpath, '/family/:fid/person/:pid');
expect(state.fullPath, '/family/:fid/person/:pid');
expect(
state.params,
state.pathParameters,
<String, String>{'fid': 'f2', 'pid': 'p1'},
);
expect(state.error, null);
expect(state.extra! as int, 4);
return PersonScreen(
state.params['fid']!, state.params['pid']!);
return PersonScreen(state.pathParameters['fid']!,
state.pathParameters['pid']!);
},
),
],
@ -585,7 +584,7 @@ void main() {
GoRoute(
path: '/family/:fid',
builder: (BuildContext context, GoRouterState state) =>
FamilyScreen(state.params['fid']!),
FamilyScreen(state.pathParameters['fid']!),
),
];
@ -595,7 +594,7 @@ void main() {
await tester.pumpAndSettle();
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
// NOTE: match the lower case, since subloc is canonicalized to match the
// NOTE: match the lower case, since location is canonicalized to match the
// path case whereas the location can be any case; so long as the path
// produces a match regardless of the location case, we win!
expect(router.location.toLowerCase(), loc.toLowerCase());
@ -1321,7 +1320,7 @@ void main() {
name: 'person',
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
expect(state.params,
expect(state.pathParameters,
<String, String>{'fid': 'f2', 'pid': 'p1'});
return const PersonScreen('dummy', 'dummy');
},
@ -1334,7 +1333,7 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
pathParameters: <String, String>{'fid': 'f2', 'pid': 'p1'});
});
testWidgets('too few params', (WidgetTester tester) async {
@ -1364,7 +1363,7 @@ void main() {
];
await expectLater(() async {
final GoRouter router = await createRouter(routes, tester);
router.goNamed('person', params: <String, String>{'fid': 'f2'});
router.goNamed('person', pathParameters: <String, String>{'fid': 'f2'});
await tester.pump();
}, throwsA(isAssertionError));
});
@ -1388,7 +1387,7 @@ void main() {
name: 'PeRsOn',
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
expect(state.params,
expect(state.pathParameters,
<String, String>{'fid': 'f2', 'pid': 'p1'});
return const PersonScreen('dummy', 'dummy');
},
@ -1401,7 +1400,7 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
pathParameters: <String, String>{'fid': 'f2', 'pid': 'p1'});
});
testWidgets('too few params', (WidgetTester tester) async {
@ -1431,7 +1430,7 @@ void main() {
await expectLater(() async {
final GoRouter router = await createRouter(routes, tester);
router.goNamed('family',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
pathParameters: <String, String>{'fid': 'f2', 'pid': 'p1'});
}, throwsA(isAssertionError));
});
@ -1445,7 +1444,7 @@ void main() {
GoRoute(
path: '/family/:fid',
builder: (BuildContext context, GoRouterState state) => FamilyScreen(
state.params['fid']!,
state.pathParameters['fid']!,
),
routes: <GoRoute>[
GoRoute(
@ -1453,8 +1452,8 @@ void main() {
path: 'person:pid',
builder: (BuildContext context, GoRouterState state) =>
PersonScreen(
state.params['fid']!,
state.params['pid']!,
state.pathParameters['fid']!,
state.pathParameters['pid']!,
),
),
],
@ -1463,7 +1462,7 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.goNamed('person',
params: <String, String>{'fid': 'f2', 'pid': 'p1'});
pathParameters: <String, String>{'fid': 'f2', 'pid': 'p1'});
await tester.pumpAndSettle();
expect(find.byType(PersonScreen), findsOneWidget);
});
@ -1476,15 +1475,15 @@ void main() {
name: 'page1',
path: '/page1/:param1',
builder: (BuildContext c, GoRouterState s) {
expect(s.params['param1'], param1);
expect(s.pathParameters['param1'], param1);
return const DummyScreen();
},
),
];
final GoRouter router = await createRouter(routes, tester);
final String loc = router
.namedLocation('page1', params: <String, String>{'param1': param1});
final String loc = router.namedLocation('page1',
pathParameters: <String, String>{'param1': param1});
router.go(loc);
await tester.pumpAndSettle();
@ -1501,7 +1500,7 @@ void main() {
name: 'page1',
path: '/page1',
builder: (BuildContext c, GoRouterState s) {
expect(s.queryParams['param1'], param1);
expect(s.queryParameters['param1'], param1);
return const DummyScreen();
},
),
@ -1509,7 +1508,7 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
final String loc = router.namedLocation('page1',
queryParams: <String, String>{'param1': param1});
queryParameters: <String, String>{'param1': param1});
router.go(loc);
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
@ -1542,7 +1541,7 @@ void main() {
final GoRouter router = await createRouter(routes, tester,
redirect: (BuildContext context, GoRouterState state) {
redirected = true;
return state.subloc == '/login' ? null : '/login';
return state.matchedLocation == '/login' ? null : '/login';
});
expect(router.location, '/login');
@ -1617,7 +1616,9 @@ void main() {
routes,
tester,
redirect: (BuildContext context, GoRouterState state) =>
state.subloc == '/login' ? null : state.namedLocation('login'),
state.matchedLocation == '/login'
? null
: state.namedLocation('login'),
);
expect(router.location, '/login');
});
@ -1682,7 +1683,7 @@ void main() {
final GoRouter router = await createRouter(routes, tester,
redirect: (BuildContext context, GoRouterState state) {
redirected = true;
return state.subloc == '/login' ? null : '/login';
return state.matchedLocation == '/login' ? null : '/login';
});
redirected = false;
// Directly set the url through platform message.
@ -1750,7 +1751,7 @@ void main() {
final GoRouter router = await createRouter(routes, tester,
redirect: (BuildContext context, GoRouterState state) =>
state.subloc == '/dummy1' ? '/dummy2' : null);
state.matchedLocation == '/dummy1' ? '/dummy2' : null);
router.go('/dummy1');
await tester.pump();
expect(router.location, '/');
@ -1759,9 +1760,9 @@ void main() {
testWidgets('top-level redirect loop', (WidgetTester tester) async {
final GoRouter router = await createRouter(<GoRoute>[], tester,
redirect: (BuildContext context, GoRouterState state) =>
state.subloc == '/'
state.matchedLocation == '/'
? '/login'
: state.subloc == '/login'
: state.matchedLocation == '/login'
? '/'
: null);
@ -1809,7 +1810,7 @@ void main() {
],
tester,
redirect: (BuildContext context, GoRouterState state) =>
state.subloc == '/' ? '/login' : null,
state.matchedLocation == '/' ? '/login' : null,
);
final List<RouteMatch> matches = router.routerDelegate.matches.matches;
@ -1826,9 +1827,9 @@ void main() {
<GoRoute>[],
tester,
redirect: (BuildContext context, GoRouterState state) =>
state.subloc == '/'
state.matchedLocation == '/'
? '/login?from=${state.location}'
: state.subloc == '/login'
: state.matchedLocation == '/login'
? '/'
: null,
);
@ -1841,7 +1842,7 @@ void main() {
expect(screen.ex, isNotNull);
});
testWidgets('expect null path/fullpath on top-level redirect',
testWidgets('expect null path/fullPath on top-level redirect',
(WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
@ -1884,12 +1885,12 @@ void main() {
initialLocation: '/login?from=/',
redirect: (BuildContext context, GoRouterState state) {
expect(Uri.parse(state.location).queryParameters, isNotEmpty);
expect(Uri.parse(state.subloc).queryParameters, isEmpty);
expect(Uri.parse(state.matchedLocation).queryParameters, isEmpty);
expect(state.path, isNull);
expect(state.fullpath, isNull);
expect(state.params.length, 0);
expect(state.queryParams.length, 1);
expect(state.queryParams['from'], '/');
expect(state.fullPath, isNull);
expect(state.pathParameters.length, 0);
expect(state.queryParameters.length, 1);
expect(state.queryParameters['from'], '/');
return null;
},
);
@ -1906,11 +1907,11 @@ void main() {
path: '/book/:bookId',
redirect: (BuildContext context, GoRouterState state) {
expect(state.location, loc);
expect(state.subloc, loc);
expect(state.matchedLocation, loc);
expect(state.path, '/book/:bookId');
expect(state.fullpath, '/book/:bookId');
expect(state.params, <String, String>{'bookId': '0'});
expect(state.queryParams.length, 0);
expect(state.fullPath, '/book/:bookId');
expect(state.pathParameters, <String, String>{'bookId': '0'});
expect(state.queryParameters.length, 0);
return null;
},
builder: (BuildContext c, GoRouterState s) => const HomeScreen(),
@ -1938,18 +1939,18 @@ void main() {
GoRoute(
path: 'family/:fid',
builder: (BuildContext c, GoRouterState s) =>
FamilyScreen(s.params['fid']!),
FamilyScreen(s.pathParameters['fid']!),
routes: <GoRoute>[
GoRoute(
path: 'person/:pid',
redirect: (BuildContext context, GoRouterState s) {
expect(s.params['fid'], 'f2');
expect(s.params['pid'], 'p1');
expect(s.pathParameters['fid'], 'f2');
expect(s.pathParameters['pid'], 'p1');
return null;
},
builder: (BuildContext c, GoRouterState s) => PersonScreen(
s.params['fid']!,
s.params['pid']!,
s.pathParameters['fid']!,
s.pathParameters['pid']!,
),
),
],
@ -2221,7 +2222,7 @@ void main() {
GoRoute(
path: '/family/:fid',
builder: (BuildContext context, GoRouterState state) =>
FamilyScreen(state.params['fid']!),
FamilyScreen(state.pathParameters['fid']!),
),
];
@ -2249,7 +2250,7 @@ void main() {
GoRoute(
path: '/family',
builder: (BuildContext context, GoRouterState state) => FamilyScreen(
state.queryParams['fid']!,
state.queryParameters['fid']!,
),
),
];
@ -2275,7 +2276,7 @@ void main() {
GoRoute(
path: '/page1/:param1',
builder: (BuildContext c, GoRouterState s) {
expect(s.params['param1'], param1);
expect(s.pathParameters['param1'], param1);
return const DummyScreen();
},
),
@ -2298,7 +2299,7 @@ void main() {
GoRoute(
path: '/page1',
builder: (BuildContext c, GoRouterState s) {
expect(s.queryParams['param1'], param1);
expect(s.queryParameters['param1'], param1);
return const DummyScreen();
},
),
@ -2346,10 +2347,10 @@ void main() {
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) {
log.info('id= ${state.params['id']}');
expect(state.params.length, 0);
expect(state.queryParams.length, 1);
expect(state.queryParams['id'], anyOf('0', '1'));
log.info('id= ${state.pathParameters['id']}');
expect(state.pathParameters.length, 0);
expect(state.queryParameters.length, 1);
expect(state.queryParameters['id'], anyOf('0', '1'));
return const HomeScreen();
},
),
@ -2359,7 +2360,7 @@ void main() {
);
final RouteMatchList matches = router.routerDelegate.matches;
expect(matches.matches, hasLength(1));
expect(matches.fullpath, '/');
expect(matches.fullPath, '/');
expect(find.byType(HomeScreen), findsOneWidget);
});
@ -2369,8 +2370,8 @@ void main() {
GoRoute(
path: '/:id',
builder: (BuildContext context, GoRouterState state) {
expect(state.params, <String, String>{'id': '0'});
expect(state.queryParams, <String, String>{'id': '1'});
expect(state.pathParameters, <String, String>{'id': '0'});
expect(state.queryParameters, <String, String>{'id': '1'});
return const HomeScreen();
},
),
@ -2382,7 +2383,7 @@ void main() {
await tester.pumpAndSettle();
final RouteMatchList matches = router.routerDelegate.matches;
expect(matches.matches, hasLength(1));
expect(matches.fullpath, '/:id');
expect(matches.fullPath, '/:id');
expect(find.byType(HomeScreen), findsOneWidget);
});
@ -2394,15 +2395,15 @@ void main() {
path: '/family',
builder: (BuildContext context, GoRouterState state) =>
FamilyScreen(
state.queryParams['fid']!,
state.queryParameters['fid']!,
),
),
GoRoute(
path: '/person',
builder: (BuildContext context, GoRouterState state) =>
PersonScreen(
state.queryParams['fid']!,
state.queryParams['pid']!,
state.queryParameters['fid']!,
state.queryParameters['pid']!,
),
),
],
@ -2470,13 +2471,13 @@ void main() {
GoRoute(
path: '/family/:fid',
builder: (BuildContext context, GoRouterState state) =>
FamilyScreen(state.params['fid']!),
FamilyScreen(state.pathParameters['fid']!),
routes: <GoRoute>[
GoRoute(
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
final String fid = state.params['fid']!;
final String pid = state.params['pid']!;
final String fid = state.pathParameters['fid']!;
final String pid = state.pathParameters['pid']!;
return PersonScreen(fid, pid);
},
@ -2536,7 +2537,7 @@ void main() {
final GoRouter router = await createRouter(routes, tester);
router.goNamed('page', queryParams: const <String, dynamic>{
router.goNamed('page', queryParameters: const <String, dynamic>{
'q1': 'v1',
'q2': <String>['v2', 'v3'],
});
@ -2694,12 +2695,12 @@ void main() {
);
key.currentContext!.namedLocation(
name,
params: params,
queryParams: queryParams,
pathParameters: params,
queryParameters: queryParams,
);
expect(router.name, name);
expect(router.params, params);
expect(router.queryParams, queryParams);
expect(router.pathParameters, params);
expect(router.queryParameters, queryParams);
});
testWidgets('calls [go] on closest GoRouter', (WidgetTester tester) async {
@ -2729,13 +2730,13 @@ void main() {
);
key.currentContext!.goNamed(
name,
params: params,
queryParams: queryParams,
pathParameters: params,
queryParameters: queryParams,
extra: extra,
);
expect(router.name, name);
expect(router.params, params);
expect(router.queryParams, queryParams);
expect(router.pathParameters, params);
expect(router.queryParameters, queryParams);
expect(router.extra, extra);
});
@ -2787,13 +2788,13 @@ void main() {
);
key.currentContext!.pushNamed(
name,
params: params,
queryParams: queryParams,
pathParameters: params,
queryParameters: queryParams,
extra: extra,
);
expect(router.name, name);
expect(router.params, params);
expect(router.queryParams, queryParams);
expect(router.pathParameters, params);
expect(router.queryParameters, queryParams);
expect(router.extra, extra);
});
@ -2810,15 +2811,15 @@ void main() {
);
final String? result = await router.pushNamed<String>(
name,
params: params,
queryParams: queryParams,
pathParameters: params,
queryParameters: queryParams,
extra: extra,
);
expect(result, extra);
expect(router.extra, extra);
expect(router.name, name);
expect(router.params, params);
expect(router.queryParams, queryParams);
expect(router.pathParameters, params);
expect(router.queryParameters, queryParams);
});
testWidgets('calls [pop] on closest GoRouter', (WidgetTester tester) async {

View File

@ -130,8 +130,8 @@ class MockGoRouter extends GoRouter {
@override
Future<T?> pushNamed<T extends Object?>(String name,
{Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
{Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra}) {
latestPushedName = name;
return Future<T?>.value();

View File

@ -17,8 +17,8 @@ void main() {
final Map<String, String> pathParameters = <String, String>{};
final RouteMatch? match = RouteMatch.match(
route: route,
restLoc: '/users/123',
parentSubloc: '',
remainingLocation: '/users/123',
matchedLocation: '',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
@ -26,14 +26,14 @@ void main() {
fail('Null match');
}
expect(match.route, route);
expect(match.subloc, '/users/123');
expect(match.matchedLocation, '/users/123');
expect(pathParameters['userId'], '123');
expect(match.extra, const _Extra('foo'));
expect(match.error, isNull);
expect(match.pageKey, isNotNull);
});
test('subloc', () {
test('matchedLocation', () {
final GoRoute route = GoRoute(
path: 'users/:userId',
builder: _builder,
@ -41,8 +41,8 @@ void main() {
final Map<String, String> pathParameters = <String, String>{};
final RouteMatch? match = RouteMatch.match(
route: route,
restLoc: 'users/123',
parentSubloc: '/home',
remainingLocation: 'users/123',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
@ -50,7 +50,7 @@ void main() {
fail('Null match');
}
expect(match.route, route);
expect(match.subloc, '/home/users/123');
expect(match.matchedLocation, '/home/users/123');
expect(pathParameters['userId'], '123');
expect(match.extra, const _Extra('foo'));
expect(match.error, isNull);
@ -70,8 +70,8 @@ void main() {
final Map<String, String> pathParameters = <String, String>{};
final RouteMatch? match = RouteMatch.match(
route: route,
restLoc: 'users/123',
parentSubloc: '/home',
remainingLocation: 'users/123',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
@ -94,16 +94,16 @@ void main() {
final Map<String, String> pathParameters = <String, String>{};
final RouteMatch? match1 = RouteMatch.match(
route: route,
restLoc: 'users/123',
parentSubloc: '/home',
remainingLocation: 'users/123',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
final RouteMatch? match2 = RouteMatch.match(
route: route,
restLoc: 'users/1234',
parentSubloc: '/home',
remainingLocation: 'users/1234',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo1'),
);
@ -119,16 +119,16 @@ void main() {
final Map<String, String> pathParameters = <String, String>{};
final RouteMatch? match1 = RouteMatch.match(
route: route,
restLoc: 'users/123',
parentSubloc: '/home',
remainingLocation: 'users/123',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo'),
);
final RouteMatch? match2 = RouteMatch.match(
route: route,
restLoc: 'users/1234',
parentSubloc: '/home',
remainingLocation: 'users/1234',
matchedLocation: '/home',
pathParameters: pathParameters,
extra: const _Extra('foo1'),
);

View File

@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/src/configuration.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/matching.dart';
import 'package:go_router/src/router.dart';
@ -27,4 +28,50 @@ void main() {
final RouteMatchList matches = router.routerDelegate.matches;
expect(matches.toString(), contains('/page-0'));
});
test('RouteMatchList compares', () async {
final GoRoute route = GoRoute(
path: '/page-0',
builder: (BuildContext context, GoRouterState state) =>
const Placeholder(),
);
final Map<String, String> params1 = <String, String>{};
final RouteMatch match1 = RouteMatch.match(
route: route,
remainingLocation: '/page-0',
matchedLocation: '',
pathParameters: params1,
extra: null,
)!;
final Map<String, String> params2 = <String, String>{};
final RouteMatch match2 = RouteMatch.match(
route: route,
remainingLocation: '/page-0',
matchedLocation: '',
pathParameters: params2,
extra: null,
)!;
final RouteMatchList matches1 = RouteMatchList(
matches: <RouteMatch>[match1],
uri: Uri.parse(''),
pathParameters: params1,
);
final RouteMatchList matches2 = RouteMatchList(
matches: <RouteMatch>[match2],
uri: Uri.parse(''),
pathParameters: params2,
);
final RouteMatchList matches3 = RouteMatchList(
matches: <RouteMatch>[match2],
uri: Uri.parse('/page-0'),
pathParameters: params2,
);
expect(matches1 == matches2, isTrue);
expect(matches1 == matches3, isFalse);
});
}

View File

@ -65,7 +65,7 @@ void main() {
expect(matches.length, 1);
expect(matchesObj.uri.toString(), '/');
expect(matches[0].extra, isNull);
expect(matches[0].subloc, '/');
expect(matches[0].matchedLocation, '/');
expect(matches[0].route, routes[0]);
final Object extra = Object();
@ -75,11 +75,11 @@ void main() {
expect(matches.length, 2);
expect(matchesObj.uri.toString(), '/abc?def=ghi');
expect(matches[0].extra, extra);
expect(matches[0].subloc, '/');
expect(matches[0].matchedLocation, '/');
expect(matches[0].route, routes[0]);
expect(matches[1].extra, extra);
expect(matches[1].subloc, '/abc');
expect(matches[1].matchedLocation, '/abc');
expect(matches[1].route, routes[0].routes[0]);
});
@ -126,11 +126,11 @@ void main() {
expect(configuration.namedLocation('lowercase'), '/abc');
expect(
configuration.namedLocation('lowercase',
queryParams: const <String, String>{'q': '1'}),
queryParameters: const <String, String>{'q': '1'}),
'/abc?q=1');
expect(
configuration.namedLocation('lowercase',
queryParams: const <String, String>{'q': '1', 'g': '2'}),
queryParameters: const <String, String>{'q': '1', 'g': '2'}),
'/abc?q=1&g=2');
});
@ -160,7 +160,7 @@ void main() {
expect(
configuration
.namedLocation('routeName', queryParams: const <String, dynamic>{
.namedLocation('routeName', queryParameters: const <String, dynamic>{
'q1': 'v1',
'q2': <String>['v2', 'v3'],
}),
@ -198,7 +198,7 @@ void main() {
expect(matches.length, 1);
expect(matchesObj.uri.toString(), '/def');
expect(matches[0].extra, isNull);
expect(matches[0].subloc, '/def');
expect(matches[0].matchedLocation, '/def');
expect(matches[0].error!.toString(),
'Exception: no routes for location: /def');
});
@ -268,10 +268,10 @@ void main() {
expect(matchesObj.pathParameters['uid'], '123');
expect(matchesObj.pathParameters['fid'], '456');
expect(matches[0].extra, isNull);
expect(matches[0].subloc, '/');
expect(matches[0].matchedLocation, '/');
expect(matches[1].extra, isNull);
expect(matches[1].subloc, '/123/family/456');
expect(matches[1].matchedLocation, '/123/family/456');
});
testWidgets(
@ -309,9 +309,9 @@ void main() {
expect(matches.length, 2);
expect(matchesObj.uri.toString(), '/123/family/345');
expect(matches[0].subloc, '/');
expect(matches[0].matchedLocation, '/');
expect(matches[1].subloc, '/123/family/345');
expect(matches[1].matchedLocation, '/123/family/345');
});
testWidgets(
@ -349,9 +349,9 @@ void main() {
expect(matches.length, 2);
expect(matchesObj.uri.toString(), '/123/family/345');
expect(matches[0].subloc, '/');
expect(matches[0].matchedLocation, '/');
expect(matches[1].subloc, '/123/family/345');
expect(matches[1].matchedLocation, '/123/family/345');
});
testWidgets(

View File

@ -37,18 +37,18 @@ class GoRouterNamedLocationSpy extends GoRouter {
GoRouterNamedLocationSpy({required super.routes});
String? name;
Map<String, String>? params;
Map<String, dynamic>? queryParams;
Map<String, String>? pathParameters;
Map<String, dynamic>? queryParameters;
@override
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
}) {
this.name = name;
this.params = params;
this.queryParams = queryParams;
this.pathParameters = pathParameters;
this.queryParameters = queryParameters;
return '';
}
}
@ -70,20 +70,20 @@ class GoRouterGoNamedSpy extends GoRouter {
GoRouterGoNamedSpy({required super.routes});
String? name;
Map<String, String>? params;
Map<String, dynamic>? queryParams;
Map<String, String>? pathParameters;
Map<String, dynamic>? queryParameters;
Object? extra;
@override
void goNamed(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) {
this.name = name;
this.params = params;
this.queryParams = queryParams;
this.pathParameters = pathParameters;
this.queryParameters = queryParameters;
this.extra = extra;
}
}
@ -106,20 +106,20 @@ class GoRouterPushNamedSpy extends GoRouter {
GoRouterPushNamedSpy({required super.routes});
String? name;
Map<String, String>? params;
Map<String, dynamic>? queryParams;
Map<String, String>? pathParameters;
Map<String, dynamic>? queryParameters;
Object? extra;
@override
Future<T?> pushNamed<T extends Object?>(
String name, {
Map<String, String> params = const <String, String>{},
Map<String, dynamic> queryParams = const <String, dynamic>{},
Map<String, String> pathParameters = const <String, String>{},
Map<String, dynamic> queryParameters = const <String, dynamic>{},
Object? extra,
}) {
this.name = name;
this.params = params;
this.queryParams = queryParams;
this.pathParameters = pathParameters;
this.queryParameters = queryParameters;
this.extra = extra;
return Future<T?>.value(extra as T?);
}

View File

@ -0,0 +1,17 @@
## Directory contents
The Dart files and golden master `.expect` files in this directory are used to
test the [`dart fix` framework](https://dart.dev/tools/dart-fix) refactorings
used by the go_router package
See the packages/packages/go_router/lib/fix_data.yaml directory for the current
package:go_router data-driven fixes.
To run these tests locally, execute this command in the
packages/packages/go_router/test_fixes directory.
```sh
dart fix --compare-to-golden
```
For more documentation about Data Driven Fixes, see
https://dart.dev/go/data-driven-fixes#test-folder.

View File

@ -0,0 +1 @@
# This ensures that parent analysis options do not accidentally break the fix tests.

View File

@ -0,0 +1,44 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:go_router/go_router.dart';
void main() {
const GoRouterState state = GoRouterState();
final GoRouter router = GoRouter(routes: <RouteBase>[]);
state.fullpath;
state.params;
state.subloc;
state.queryParams;
state.namedLocation(
'name',
params: <String, String>{},
queryParams: <String, String>{},
);
router.namedLocation(
'name',
params: <String, String>{},
queryParams: <String, String>{},
);
router.goNamed(
'name',
params: <String, String>{},
queryParams: <String, String>{},
);
router.pushNamed(
'name',
params: <String, String>{},
queryParams: <String, String>{},
);
router.pushReplacementNamed(
'name',
params: <String, String>{},
queryParams: <String, String>{},
);
router.replaceNamed(
'name',
params: <String, String>{},
queryParams: <String, String>{},
);
}

View File

@ -0,0 +1,44 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:go_router/go_router.dart';
void main() {
const GoRouterState state = GoRouterState();
final GoRouter router = GoRouter(routes: <RouteBase>[]);
state.fullPath;
state.pathParameters;
state.matchedLocation;
state.queryParameters;
state.namedLocation(
'name',
pathParameters: <String, String>{},
queryParameters: <String, String>{},
);
router.namedLocation(
'name',
pathParameters: <String, String>{},
queryParameters: <String, String>{},
);
router.goNamed(
'name',
pathParameters: <String, String>{},
queryParameters: <String, String>{},
);
router.pushNamed(
'name',
pathParameters: <String, String>{},
queryParameters: <String, String>{},
);
router.pushReplacementNamed(
'name',
pathParameters: <String, String>{},
queryParameters: <String, String>{},
);
router.replaceNamed(
'name',
pathParameters: <String, String>{},
queryParameters: <String, String>{},
);
}

View File

@ -0,0 +1,51 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Called from the custom-tests CI action.
//
// usage: dart run tool/run_tests.dart
// ignore_for_file: avoid_print
import 'dart:io';
import 'package:path/path.dart' as p;
Future<void> main(List<String> args) async {
if (!Platform.isMacOS) {
print('This test can only be run on macOS.');
exit(0);
}
final Directory packageRoot =
Directory(p.dirname(Platform.script.path)).parent;
final int status = await _runProcess(
'dart',
<String>[
'fix',
'--compare-to-golden',
],
workingDirectory: p.join(packageRoot.path, 'test_fixes'),
);
exit(status);
}
Future<Process> _streamOutput(Future<Process> processFuture) async {
final Process process = await processFuture;
stdout.addStream(process.stdout);
stderr.addStream(process.stderr);
return process;
}
Future<int> _runProcess(
String command,
List<String> arguments, {
String? workingDirectory,
}) async {
final Process process = await _streamOutput(Process.start(
command,
arguments,
workingDirectory: workingDirectory,
));
return process.exitCode;
}

View File

@ -23,3 +23,5 @@
- rfw/example
# Disables docs requirements, as it is test code.
- web_benchmarks/testing/test_app
# Has some test files that are intentionally broken to conduct dart fix tests.
- go_router