mirror of
https://github.com/flutter/packages.git
synced 2025-07-03 00:49:32 +08:00
[go_router] adds ability to dynamically update routing table. (#5079)
fixes https://github.com/flutter/flutter/issues/136005
This commit is contained in:
@ -1,3 +1,10 @@
|
||||
## 12.0.0
|
||||
|
||||
- Adds ability to dynamically update routing table.
|
||||
- **BREAKING CHANGE**:
|
||||
- The function signature of constructor of `RouteConfiguration` is updated.
|
||||
- Adds a required `matchedPath` named parameter to `RouteMatch.match`.
|
||||
|
||||
## 11.1.4
|
||||
|
||||
- Fixes missing parameters in the type-safe routes topic documentation.
|
||||
|
@ -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 12.0.0](https://flutter.dev/go/go-router-v12-breaking-changes).
|
||||
- [Migrating to 11.0.0](https://flutter.dev/go/go-router-v11-breaking-changes).
|
||||
- [Migrating to 10.0.0](https://flutter.dev/go/go-router-v10-breaking-changes).
|
||||
- [Migrating to 9.0.0](https://flutter.dev/go/go-router-v9-breaking-changes).
|
||||
|
@ -85,6 +85,35 @@ GoRoute(
|
||||
)
|
||||
```
|
||||
|
||||
# Dynamic RoutingConfig
|
||||
The [RoutingConfig][] provides a way to update the GoRoute\[s\] after
|
||||
the [GoRouter][] has already created. This can be done by creating a GoRouter
|
||||
with special constructor [GoRouter.routingConfig][]
|
||||
|
||||
```dart
|
||||
final ValueNotifier<RoutingConfig> myRoutingConfig = ValueNotifier<RoutingConfig>(
|
||||
RoutingConfig(
|
||||
routes: <RouteBase>[GoRoute(path: '/', builder: (_, __) => HomeScreen())],
|
||||
),
|
||||
);
|
||||
final GoRouter router = GoRouter.routingConfig(routingConfig: myRoutingConfig);
|
||||
```
|
||||
|
||||
To change the GoRoute later, modify the value of the [ValueNotifier][] directly.
|
||||
|
||||
```dart
|
||||
myRoutingConfig.value = RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(path: '/', builder: (_, __) => AlternativeHomeScreen()),
|
||||
GoRoute(path: '/a-new-route', builder: (_, __) => SomeScreen()),
|
||||
],
|
||||
);
|
||||
```
|
||||
|
||||
The value change is automatically picked up by GoRouter and causes it to reparse
|
||||
the current routes, i.e. RouteMatchList, stored in GoRouter. The RouteMatchList will
|
||||
reflect the latest change of the `RoutingConfig`.
|
||||
|
||||
# Nested navigation
|
||||
Some apps display destinations in a subsection of the screen, for example, an
|
||||
app using a BottomNavigationBar that stays on-screen when navigating between
|
||||
|
108
packages/go_router/example/lib/routing_config.dart
Normal file
108
packages/go_router/example/lib/routing_config.dart
Normal file
@ -0,0 +1,108 @@
|
||||
// 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:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// This app shows how to dynamically add more route into routing config
|
||||
void main() => runApp(const MyApp());
|
||||
|
||||
/// The main app.
|
||||
class MyApp extends StatefulWidget {
|
||||
/// Constructs a [MyApp]
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
bool isNewRouteAdded = false;
|
||||
|
||||
late final ValueNotifier<RoutingConfig> myConfig =
|
||||
ValueNotifier<RoutingConfig>(_generateRoutingConfig());
|
||||
|
||||
late final GoRouter router = GoRouter.routingConfig(
|
||||
routingConfig: myConfig,
|
||||
errorBuilder: (_, GoRouterState state) => Scaffold(
|
||||
appBar: AppBar(title: const Text('Page not found')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text('${state.uri} does not exist'),
|
||||
ElevatedButton(
|
||||
onPressed: () => router.go('/'),
|
||||
child: const Text('Go to home')),
|
||||
],
|
||||
)),
|
||||
));
|
||||
|
||||
RoutingConfig _generateRoutingConfig() {
|
||||
return RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (_, __) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Home')),
|
||||
body: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: isNewRouteAdded
|
||||
? null
|
||||
: () {
|
||||
setState(() {
|
||||
isNewRouteAdded = true;
|
||||
// Modify the routing config.
|
||||
myConfig.value = _generateRoutingConfig();
|
||||
});
|
||||
},
|
||||
child: isNewRouteAdded
|
||||
? const Text('A route has been added')
|
||||
: const Text('Add a new route'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
router.go('/new-route');
|
||||
},
|
||||
child: const Text('Try going to /new-route'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (isNewRouteAdded)
|
||||
GoRoute(
|
||||
path: '/new-route',
|
||||
builder: (_, __) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('A new Route')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => router.go('/'),
|
||||
child: const Text('Go to home')),
|
||||
],
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: router,
|
||||
);
|
||||
}
|
||||
}
|
31
packages/go_router/example/test/routing_config_test.dart
Normal file
31
packages/go_router/example/test/routing_config_test.dart
Normal file
@ -0,0 +1,31 @@
|
||||
// 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:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router_examples/routing_config.dart' as example;
|
||||
|
||||
void main() {
|
||||
testWidgets('example works', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.MyApp());
|
||||
expect(find.text('Add a new route'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Try going to /new-route'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Page not found'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Go to home'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Add a new route'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('A route has been added'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Try going to /new-route'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('A new Route'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'logging.dart';
|
||||
@ -11,6 +12,7 @@ import 'match.dart';
|
||||
import 'misc/errors.dart';
|
||||
import 'path_utils.dart';
|
||||
import 'route.dart';
|
||||
import 'router.dart';
|
||||
import 'state.dart';
|
||||
|
||||
/// The signature of the redirect callback.
|
||||
@ -20,19 +22,12 @@ typedef GoRouterRedirect = FutureOr<String?> Function(
|
||||
/// The route configuration for GoRouter configured by the app.
|
||||
class RouteConfiguration {
|
||||
/// Constructs a [RouteConfiguration].
|
||||
RouteConfiguration({
|
||||
required this.routes,
|
||||
required this.redirectLimit,
|
||||
required this.topRedirect,
|
||||
RouteConfiguration(
|
||||
this._routingConfig, {
|
||||
required this.navigatorKey,
|
||||
}) : assert(_debugCheckPath(routes, true)),
|
||||
assert(
|
||||
_debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
|
||||
assert(_debugCheckParentNavigatorKeys(
|
||||
routes, <GlobalKey<NavigatorState>>[navigatorKey])) {
|
||||
assert(_debugCheckStatefulShellBranchDefaultLocations(routes));
|
||||
_cacheNameToPath('', routes);
|
||||
log(debugKnownRoutes());
|
||||
}) {
|
||||
_onRoutingTableChanged();
|
||||
_routingConfig.addListener(_onRoutingTableChanged);
|
||||
}
|
||||
|
||||
static bool _debugCheckPath(List<RouteBase> routes, bool isTopLevel) {
|
||||
@ -40,15 +35,11 @@ class RouteConfiguration {
|
||||
late bool subRouteIsTopLevel;
|
||||
if (route is GoRoute) {
|
||||
if (isTopLevel) {
|
||||
if (!route.path.startsWith('/')) {
|
||||
throw GoError('top-level path must start with "/": $route');
|
||||
}
|
||||
assert(route.path.startsWith('/'),
|
||||
'top-level path must start with "/": $route');
|
||||
} else {
|
||||
if (route.path.startsWith('/') || route.path.endsWith('/')) {
|
||||
throw GoError(
|
||||
'sub-route path may not start or end with "/": $route',
|
||||
);
|
||||
}
|
||||
assert(!route.path.startsWith('/') && !route.path.endsWith('/'),
|
||||
'sub-route path may not start or end with "/": $route');
|
||||
}
|
||||
subRouteIsTopLevel = false;
|
||||
} else if (route is ShellRouteBase) {
|
||||
@ -69,11 +60,11 @@ class RouteConfiguration {
|
||||
if (parentKey != null) {
|
||||
// Verify that the root navigator or a ShellRoute ancestor has a
|
||||
// matching navigator key.
|
||||
if (!allowedKeys.contains(parentKey)) {
|
||||
throw GoError('parentNavigatorKey $parentKey must refer to'
|
||||
" an ancestor ShellRoute's navigatorKey or GoRouter's"
|
||||
' navigatorKey');
|
||||
}
|
||||
assert(
|
||||
allowedKeys.contains(parentKey),
|
||||
'parentNavigatorKey $parentKey must refer to'
|
||||
" an ancestor ShellRoute's navigatorKey or GoRouter's"
|
||||
' navigatorKey');
|
||||
|
||||
_debugCheckParentNavigatorKeys(
|
||||
route.routes,
|
||||
@ -98,11 +89,10 @@ class RouteConfiguration {
|
||||
);
|
||||
} else if (route is StatefulShellRoute) {
|
||||
for (final StatefulShellBranch branch in route.branches) {
|
||||
if (allowedKeys.contains(branch.navigatorKey)) {
|
||||
throw GoError(
|
||||
'StatefulShellBranch must not reuse an ancestor navigatorKey '
|
||||
'(${branch.navigatorKey})');
|
||||
}
|
||||
assert(
|
||||
!allowedKeys.contains(branch.navigatorKey),
|
||||
'StatefulShellBranch must not reuse an ancestor navigatorKey '
|
||||
'(${branch.navigatorKey})');
|
||||
|
||||
_debugCheckParentNavigatorKeys(
|
||||
branch.routes,
|
||||
@ -149,23 +139,20 @@ class RouteConfiguration {
|
||||
final GoRoute? route = branch.defaultRoute;
|
||||
final String? initialLocation =
|
||||
route != null ? locationForRoute(route) : null;
|
||||
if (initialLocation == null) {
|
||||
throw GoError(
|
||||
'The default location of a StatefulShellBranch must be '
|
||||
'derivable from GoRoute descendant');
|
||||
}
|
||||
if (route!.pathParameters.isNotEmpty) {
|
||||
throw GoError(
|
||||
'The default location of a StatefulShellBranch cannot be '
|
||||
'a parameterized route');
|
||||
}
|
||||
assert(
|
||||
initialLocation != null,
|
||||
'The default location of a StatefulShellBranch must be '
|
||||
'derivable from GoRoute descendant');
|
||||
assert(
|
||||
route!.pathParameters.isEmpty,
|
||||
'The default location of a StatefulShellBranch cannot be '
|
||||
'a parameterized route');
|
||||
} else {
|
||||
final RouteMatchList matchList = findMatch(branch.initialLocation!);
|
||||
if (matchList.isError) {
|
||||
throw GoError(
|
||||
'initialLocation (${matchList.uri}) of StatefulShellBranch must '
|
||||
'be a valid location');
|
||||
}
|
||||
assert(
|
||||
!matchList.isError,
|
||||
'initialLocation (${matchList.uri}) of StatefulShellBranch must '
|
||||
'be a valid location');
|
||||
final List<RouteBase> matchRoutes = matchList.routes;
|
||||
final int shellIndex = matchRoutes.indexOf(route);
|
||||
bool matchFound = false;
|
||||
@ -173,12 +160,11 @@ class RouteConfiguration {
|
||||
final RouteBase branchRoot = matchRoutes[shellIndex + 1];
|
||||
matchFound = branch.routes.contains(branchRoot);
|
||||
}
|
||||
if (!matchFound) {
|
||||
throw GoError(
|
||||
'The initialLocation (${branch.initialLocation}) of '
|
||||
'StatefulShellBranch must match a descendant route of the '
|
||||
'branch');
|
||||
}
|
||||
assert(
|
||||
matchFound,
|
||||
'The initialLocation (${branch.initialLocation}) of '
|
||||
'StatefulShellBranch must match a descendant route of the '
|
||||
'branch');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -202,6 +188,18 @@ class RouteConfiguration {
|
||||
);
|
||||
}
|
||||
|
||||
void _onRoutingTableChanged() {
|
||||
final RoutingConfig routingTable = _routingConfig.value;
|
||||
assert(_debugCheckPath(routingTable.routes, true));
|
||||
assert(_debugVerifyNoDuplicatePathParameter(
|
||||
routingTable.routes, <String, GoRoute>{}));
|
||||
assert(_debugCheckParentNavigatorKeys(
|
||||
routingTable.routes, <GlobalKey<NavigatorState>>[navigatorKey]));
|
||||
assert(_debugCheckStatefulShellBranchDefaultLocations(routingTable.routes));
|
||||
_cacheNameToPath('', routingTable.routes);
|
||||
log(debugKnownRoutes());
|
||||
}
|
||||
|
||||
/// Builds a [GoRouterState] suitable for top level callback such as
|
||||
/// `GoRouter.redirect` or `GoRouter.onException`.
|
||||
GoRouterState buildTopLevelGoRouterState(RouteMatchList matchList) {
|
||||
@ -218,18 +216,21 @@ class RouteConfiguration {
|
||||
);
|
||||
}
|
||||
|
||||
/// The routing table.
|
||||
final ValueListenable<RoutingConfig> _routingConfig;
|
||||
|
||||
/// The list of top level routes used by [GoRouterDelegate].
|
||||
final List<RouteBase> routes;
|
||||
List<RouteBase> get routes => _routingConfig.value.routes;
|
||||
|
||||
/// Top level page redirect.
|
||||
GoRouterRedirect get topRedirect => _routingConfig.value.redirect;
|
||||
|
||||
/// The limit for the number of consecutive redirects.
|
||||
final int redirectLimit;
|
||||
int get redirectLimit => _routingConfig.value.redirectLimit;
|
||||
|
||||
/// The global key for top level navigator.
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
/// Top level page redirect.
|
||||
final GoRouterRedirect topRedirect;
|
||||
|
||||
final Map<String, String> _nameToPath = <String, String>{};
|
||||
|
||||
/// Looks up the url location by a [GoRoute]'s name.
|
||||
@ -294,14 +295,32 @@ class RouteConfiguration {
|
||||
extra: extra);
|
||||
}
|
||||
|
||||
/// Reparse the input RouteMatchList
|
||||
RouteMatchList reparse(RouteMatchList matchList) {
|
||||
RouteMatchList result =
|
||||
findMatch(matchList.uri.toString(), extra: matchList.extra);
|
||||
|
||||
for (final ImperativeRouteMatch imperativeMatch
|
||||
in matchList.matches.whereType<ImperativeRouteMatch>()) {
|
||||
final ImperativeRouteMatch match = ImperativeRouteMatch(
|
||||
pageKey: imperativeMatch.pageKey,
|
||||
matches: findMatch(imperativeMatch.matches.uri.toString(),
|
||||
extra: imperativeMatch.matches.extra),
|
||||
completer: imperativeMatch.completer);
|
||||
result = result.push(match);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<RouteMatch>? _getLocRouteMatches(
|
||||
Uri uri, Map<String, String> pathParameters) {
|
||||
final List<RouteMatch>? result = _getLocRouteRecursively(
|
||||
location: uri.path,
|
||||
remainingLocation: uri.path,
|
||||
matchedLocation: '',
|
||||
matchedPath: '',
|
||||
pathParameters: pathParameters,
|
||||
routes: routes,
|
||||
routes: _routingConfig.value.routes,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
@ -310,6 +329,7 @@ class RouteConfiguration {
|
||||
required String location,
|
||||
required String remainingLocation,
|
||||
required String matchedLocation,
|
||||
required String matchedPath,
|
||||
required Map<String, String> pathParameters,
|
||||
required List<RouteBase> routes,
|
||||
}) {
|
||||
@ -323,6 +343,7 @@ class RouteConfiguration {
|
||||
route: route,
|
||||
remainingLocation: remainingLocation,
|
||||
matchedLocation: matchedLocation,
|
||||
matchedPath: matchedPath,
|
||||
pathParameters: subPathParameters,
|
||||
);
|
||||
|
||||
@ -343,9 +364,11 @@ class RouteConfiguration {
|
||||
// Otherwise, recurse
|
||||
final String childRestLoc;
|
||||
final String newParentSubLoc;
|
||||
final String newParentPath;
|
||||
if (match.route is ShellRouteBase) {
|
||||
childRestLoc = remainingLocation;
|
||||
newParentSubLoc = matchedLocation;
|
||||
newParentPath = matchedPath;
|
||||
} else {
|
||||
assert(location.startsWith(match.matchedLocation));
|
||||
assert(remainingLocation.isNotEmpty);
|
||||
@ -353,12 +376,15 @@ class RouteConfiguration {
|
||||
childRestLoc = location.substring(match.matchedLocation.length +
|
||||
(match.matchedLocation == '/' ? 0 : 1));
|
||||
newParentSubLoc = match.matchedLocation;
|
||||
newParentPath =
|
||||
concatenatePaths(matchedPath, (match.route as GoRoute).path);
|
||||
}
|
||||
|
||||
final List<RouteMatch>? subRouteMatch = _getLocRouteRecursively(
|
||||
location: location,
|
||||
remainingLocation: childRestLoc,
|
||||
matchedLocation: newParentSubLoc,
|
||||
matchedPath: newParentPath,
|
||||
pathParameters: subPathParameters,
|
||||
routes: route.routes,
|
||||
);
|
||||
@ -437,7 +463,7 @@ class RouteConfiguration {
|
||||
|
||||
redirectHistory.add(prevMatchList);
|
||||
// Check for top-level redirect
|
||||
final FutureOr<String?> topRedirectResult = topRedirect(
|
||||
final FutureOr<String?> topRedirectResult = _routingConfig.value.redirect(
|
||||
context,
|
||||
buildTopLevelGoRouterState(prevMatchList),
|
||||
);
|
||||
@ -520,7 +546,7 @@ class RouteConfiguration {
|
||||
newMatch
|
||||
])}');
|
||||
}
|
||||
if (redirects.length > redirectLimit) {
|
||||
if (redirects.length > _routingConfig.value.redirectLimit) {
|
||||
throw GoException(
|
||||
'too many redirects ${_formatRedirectionHistory(<RouteMatchList>[
|
||||
...redirects,
|
||||
@ -545,11 +571,11 @@ class RouteConfiguration {
|
||||
/// Builds the absolute path for the route, by concatenating the paths of the
|
||||
/// route and all its ancestors.
|
||||
String? locationForRoute(RouteBase route) =>
|
||||
fullPathForRoute(route, '', routes);
|
||||
fullPathForRoute(route, '', _routingConfig.value.routes);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'RouterConfiguration: $routes';
|
||||
return 'RouterConfiguration: ${_routingConfig.value.routes}';
|
||||
}
|
||||
|
||||
/// Returns the full path of [routes].
|
||||
@ -560,7 +586,7 @@ class RouteConfiguration {
|
||||
String debugKnownRoutes() {
|
||||
final StringBuffer sb = StringBuffer();
|
||||
sb.writeln('Full paths for routes:');
|
||||
_debugFullPathsFor(routes, '', 0, sb);
|
||||
_debugFullPathsFor(_routingConfig.value.routes, '', 0, sb);
|
||||
|
||||
if (_nameToPath.isNotEmpty) {
|
||||
sb.writeln('known full paths for route names:');
|
||||
|
@ -34,6 +34,7 @@ class RouteMatch {
|
||||
/// into `pathParameters`.
|
||||
static RouteMatch? match({
|
||||
required RouteBase route,
|
||||
required String matchedPath, // e.g. /family/:fid
|
||||
required String remainingLocation, // e.g. person/p1
|
||||
required String matchedLocation, // e.g. /family/f2
|
||||
required Map<String, String> pathParameters,
|
||||
@ -59,10 +60,11 @@ class RouteMatch {
|
||||
final String pathLoc = patternToPath(route.path, encodedParams);
|
||||
final String newMatchedLocation =
|
||||
concatenatePaths(matchedLocation, pathLoc);
|
||||
final String newMatchedPath = concatenatePaths(matchedPath, route.path);
|
||||
return RouteMatch(
|
||||
route: route,
|
||||
matchedLocation: newMatchedLocation,
|
||||
pageKey: ValueKey<String>(route.hashCode.toString()),
|
||||
pageKey: ValueKey<String>(newMatchedPath),
|
||||
);
|
||||
}
|
||||
assert(false, 'Unexpected route type: $route');
|
||||
@ -136,16 +138,16 @@ class ImperativeRouteMatch extends RouteMatch {
|
||||
completer.complete(value);
|
||||
}
|
||||
|
||||
// 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);
|
||||
return other is ImperativeRouteMatch &&
|
||||
completer == other.completer &&
|
||||
matches == other.matches &&
|
||||
super == other;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => identityHashCode(this);
|
||||
int get hashCode => Object.hash(super.hashCode, completer, matches.hashCode);
|
||||
}
|
||||
|
||||
/// The list of [RouteMatch] objects.
|
||||
|
@ -195,9 +195,9 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
|
||||
return newMatchList;
|
||||
case NavigatingType.restore:
|
||||
// Still need to consider redirection.
|
||||
return baseRouteMatchList!.uri.toString() == newMatchList.uri.toString()
|
||||
? baseRouteMatchList
|
||||
: newMatchList;
|
||||
return baseRouteMatchList!.uri.toString() != newMatchList.uri.toString()
|
||||
? newMatchList
|
||||
: baseRouteMatchList;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,9 @@
|
||||
// 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/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'configuration.dart';
|
||||
@ -9,7 +12,6 @@ import 'delegate.dart';
|
||||
import 'information_provider.dart';
|
||||
import 'logging.dart';
|
||||
import 'match.dart';
|
||||
import 'misc/errors.dart';
|
||||
import 'misc/inherited_router.dart';
|
||||
import 'parser.dart';
|
||||
import 'route.dart';
|
||||
@ -24,10 +26,57 @@ typedef GoExceptionHandler = void Function(
|
||||
GoRouter router,
|
||||
);
|
||||
|
||||
/// A set of parameters that defines routing in GoRouter.
|
||||
///
|
||||
/// This is typically used with [GoRouter.routingConfig] to create a go router
|
||||
/// with dynamic routing config.
|
||||
///
|
||||
/// See [routing_config.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/routing_config.dart).
|
||||
///
|
||||
/// {@category Configuration}
|
||||
class RoutingConfig {
|
||||
/// Creates a routing config.
|
||||
///
|
||||
/// The [routes] must not be empty.
|
||||
const RoutingConfig({
|
||||
required this.routes,
|
||||
this.redirect = _defaultRedirect,
|
||||
this.redirectLimit = 5,
|
||||
});
|
||||
|
||||
static FutureOr<String?> _defaultRedirect(
|
||||
BuildContext context, GoRouterState state) =>
|
||||
null;
|
||||
|
||||
/// The supported routes.
|
||||
///
|
||||
/// The `routes` list specifies the top-level routes for the app. It must not be
|
||||
/// empty and must contain an [GoRoute] to match `/`.
|
||||
///
|
||||
/// See [GoRouter].
|
||||
final List<RouteBase> routes;
|
||||
|
||||
/// The top-level callback allows the app to redirect to a new location.
|
||||
///
|
||||
/// Alternatively, you can specify a redirect for an individual route using
|
||||
/// [GoRoute.redirect]. If [BuildContext.dependOnInheritedWidgetOfExactType] is
|
||||
/// used during the redirection (which is how `of` methods are usually
|
||||
/// implemented), a re-evaluation will be triggered when the [InheritedWidget]
|
||||
/// changes.
|
||||
///
|
||||
/// See [GoRouter].
|
||||
final GoRouterRedirect redirect;
|
||||
|
||||
/// The maximum number of redirection allowed.
|
||||
///
|
||||
/// See [GoRouter].
|
||||
final int redirectLimit;
|
||||
}
|
||||
|
||||
/// The route configuration for the app.
|
||||
///
|
||||
/// The `routes` list specifies the top-level routes for the app. It must not be
|
||||
/// empty and must contain an [GoRouter] to match `/`.
|
||||
/// empty and must contain an [GoRoute] to match `/`.
|
||||
///
|
||||
/// See the [Get
|
||||
/// started](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/main.dart)
|
||||
@ -70,7 +119,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
/// and an error page builder.
|
||||
///
|
||||
/// The `routes` must not be null and must contain an [GoRouter] to match `/`.
|
||||
GoRouter({
|
||||
factory GoRouter({
|
||||
required List<RouteBase> routes,
|
||||
GoExceptionHandler? onException,
|
||||
GoRouterPageBuilder? errorPageBuilder,
|
||||
@ -80,6 +129,48 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
int redirectLimit = 5,
|
||||
bool routerNeglect = false,
|
||||
String? initialLocation,
|
||||
bool overridePlatformDefaultLocation = false,
|
||||
Object? initialExtra,
|
||||
List<NavigatorObserver>? observers,
|
||||
bool debugLogDiagnostics = false,
|
||||
GlobalKey<NavigatorState>? navigatorKey,
|
||||
String? restorationScopeId,
|
||||
bool requestFocus = true,
|
||||
}) {
|
||||
return GoRouter.routingConfig(
|
||||
routingConfig: _ConstantRoutingConfig(
|
||||
RoutingConfig(
|
||||
routes: routes,
|
||||
redirect: redirect ?? RoutingConfig._defaultRedirect,
|
||||
redirectLimit: redirectLimit),
|
||||
),
|
||||
onException: onException,
|
||||
errorPageBuilder: errorPageBuilder,
|
||||
errorBuilder: errorBuilder,
|
||||
refreshListenable: refreshListenable,
|
||||
routerNeglect: routerNeglect,
|
||||
initialLocation: initialLocation,
|
||||
overridePlatformDefaultLocation: overridePlatformDefaultLocation,
|
||||
initialExtra: initialExtra,
|
||||
observers: observers,
|
||||
debugLogDiagnostics: debugLogDiagnostics,
|
||||
navigatorKey: navigatorKey,
|
||||
restorationScopeId: restorationScopeId,
|
||||
requestFocus: requestFocus,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a [GoRouter] with a dynamic [RoutingConfig].
|
||||
///
|
||||
/// See [routing_config.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/routing_config.dart).
|
||||
GoRouter.routingConfig({
|
||||
required ValueListenable<RoutingConfig> routingConfig,
|
||||
GoExceptionHandler? onException,
|
||||
GoRouterPageBuilder? errorPageBuilder,
|
||||
GoRouterWidgetBuilder? errorBuilder,
|
||||
Listenable? refreshListenable,
|
||||
bool routerNeglect = false,
|
||||
String? initialLocation,
|
||||
this.overridePlatformDefaultLocation = false,
|
||||
Object? initialExtra,
|
||||
List<NavigatorObserver>? observers,
|
||||
@ -87,7 +178,8 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
GlobalKey<NavigatorState>? navigatorKey,
|
||||
String? restorationScopeId,
|
||||
bool requestFocus = true,
|
||||
}) : backButtonDispatcher = RootBackButtonDispatcher(),
|
||||
}) : _routingConfig = routingConfig,
|
||||
backButtonDispatcher = RootBackButtonDispatcher(),
|
||||
assert(
|
||||
initialExtra == null || initialLocation != null,
|
||||
'initialLocation must be set in order to use initialExtra',
|
||||
@ -99,24 +191,15 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
(errorPageBuilder == null ? 0 : 1) +
|
||||
(errorBuilder == null ? 0 : 1) <
|
||||
2,
|
||||
'Only one of onException, errorPageBuilder, or errorBuilder can be provided.'),
|
||||
assert(_debugCheckPath(routes, true)),
|
||||
assert(
|
||||
_debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
|
||||
assert(_debugCheckParentNavigatorKeys(
|
||||
routes,
|
||||
navigatorKey == null
|
||||
? <GlobalKey<NavigatorState>>[]
|
||||
: <GlobalKey<NavigatorState>>[navigatorKey])) {
|
||||
'Only one of onException, errorPageBuilder, or errorBuilder can be provided.') {
|
||||
setLogging(enabled: debugLogDiagnostics);
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
navigatorKey ??= GlobalKey<NavigatorState>();
|
||||
|
||||
_routingConfig.addListener(_handleRoutingConfigChanged);
|
||||
configuration = RouteConfiguration(
|
||||
routes: routes,
|
||||
topRedirect: redirect ?? (_, __) => null,
|
||||
redirectLimit: redirectLimit,
|
||||
_routingConfig,
|
||||
navigatorKey: navigatorKey,
|
||||
);
|
||||
|
||||
@ -166,103 +249,6 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
}());
|
||||
}
|
||||
|
||||
static bool _debugCheckPath(List<RouteBase> routes, bool isTopLevel) {
|
||||
for (final RouteBase route in routes) {
|
||||
late bool subRouteIsTopLevel;
|
||||
if (route is GoRoute) {
|
||||
if (isTopLevel) {
|
||||
assert(route.path.startsWith('/'),
|
||||
'top-level path must start with "/": $route');
|
||||
} else {
|
||||
assert(!route.path.startsWith('/') && !route.path.endsWith('/'),
|
||||
'sub-route path may not start or end with "/": $route');
|
||||
}
|
||||
subRouteIsTopLevel = false;
|
||||
} else if (route is ShellRouteBase) {
|
||||
subRouteIsTopLevel = isTopLevel;
|
||||
}
|
||||
_debugCheckPath(route.routes, subRouteIsTopLevel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check that each parentNavigatorKey refers to either a ShellRoute's
|
||||
// navigatorKey or the root navigator key.
|
||||
static bool _debugCheckParentNavigatorKeys(
|
||||
List<RouteBase> routes, List<GlobalKey<NavigatorState>> allowedKeys) {
|
||||
for (final RouteBase route in routes) {
|
||||
if (route is GoRoute) {
|
||||
final GlobalKey<NavigatorState>? parentKey = route.parentNavigatorKey;
|
||||
if (parentKey != null) {
|
||||
// Verify that the root navigator or a ShellRoute ancestor has a
|
||||
// matching navigator key.
|
||||
assert(
|
||||
allowedKeys.contains(parentKey),
|
||||
'parentNavigatorKey $parentKey must refer to'
|
||||
" an ancestor ShellRoute's navigatorKey or GoRouter's"
|
||||
' navigatorKey');
|
||||
|
||||
_debugCheckParentNavigatorKeys(
|
||||
route.routes,
|
||||
<GlobalKey<NavigatorState>>[
|
||||
// Once a parentNavigatorKey is used, only that navigator key
|
||||
// or keys above it can be used.
|
||||
...allowedKeys.sublist(0, allowedKeys.indexOf(parentKey) + 1),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
_debugCheckParentNavigatorKeys(
|
||||
route.routes,
|
||||
<GlobalKey<NavigatorState>>[
|
||||
...allowedKeys,
|
||||
],
|
||||
);
|
||||
}
|
||||
} else if (route is ShellRoute) {
|
||||
_debugCheckParentNavigatorKeys(
|
||||
route.routes,
|
||||
<GlobalKey<NavigatorState>>[...allowedKeys..add(route.navigatorKey)],
|
||||
);
|
||||
} else if (route is StatefulShellRoute) {
|
||||
for (final StatefulShellBranch branch in route.branches) {
|
||||
assert(
|
||||
!allowedKeys.contains(branch.navigatorKey),
|
||||
'StatefulShellBranch must not reuse an ancestor navigatorKey '
|
||||
'(${branch.navigatorKey})');
|
||||
|
||||
_debugCheckParentNavigatorKeys(
|
||||
branch.routes,
|
||||
<GlobalKey<NavigatorState>>[
|
||||
...allowedKeys,
|
||||
branch.navigatorKey,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool _debugVerifyNoDuplicatePathParameter(
|
||||
List<RouteBase> routes, Map<String, GoRoute> usedPathParams) {
|
||||
for (final RouteBase route in routes) {
|
||||
if (route is! GoRoute) {
|
||||
continue;
|
||||
}
|
||||
for (final String pathParam in route.pathParameters) {
|
||||
if (usedPathParams.containsKey(pathParam)) {
|
||||
final bool sameRoute = usedPathParams[pathParam] == route;
|
||||
throw GoError(
|
||||
"duplicate path parameter, '$pathParam' found in ${sameRoute ? '$route' : '${usedPathParams[pathParam]}, and $route'}");
|
||||
}
|
||||
usedPathParams[pathParam] = route;
|
||||
}
|
||||
_debugVerifyNoDuplicatePathParameter(route.routes, usedPathParams);
|
||||
route.pathParameters.forEach(usedPathParams.remove);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Whether the imperative API affects browser URL bar.
|
||||
///
|
||||
/// The Imperative APIs refer to [push], [pushReplacement], or [replace].
|
||||
@ -302,6 +288,11 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
@override
|
||||
late final GoRouteInformationParser routeInformationParser;
|
||||
|
||||
void _handleRoutingConfigChanged() {
|
||||
// Reparse is needed to update its builder
|
||||
restore(configuration.reparse(routerDelegate.currentConfiguration));
|
||||
}
|
||||
|
||||
/// Whether to ignore platform's default initial location when
|
||||
/// `initialLocation` is set.
|
||||
///
|
||||
@ -319,6 +310,8 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
/// It's advisable to only set this to [true] if one explicitly wants to.
|
||||
final bool overridePlatformDefaultLocation;
|
||||
|
||||
final ValueListenable<RoutingConfig> _routingConfig;
|
||||
|
||||
/// Returns `true` if there is at least two or more route can be pop.
|
||||
bool canPop() => routerDelegate.canPop();
|
||||
|
||||
@ -522,6 +515,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
|
||||
/// Disposes resource created by this object.
|
||||
void dispose() {
|
||||
_routingConfig.removeListener(_handleRoutingConfigChanged);
|
||||
routeInformationProvider.dispose();
|
||||
routerDelegate.dispose();
|
||||
}
|
||||
@ -543,3 +537,20 @@ class GoRouter implements RouterConfig<RouteMatchList> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A routing config that is never going to change.
|
||||
class _ConstantRoutingConfig extends ValueListenable<RoutingConfig> {
|
||||
const _ConstantRoutingConfig(this.value);
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
// Intentionally empty because listener will never be called.
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(VoidCallback listener) {
|
||||
// Intentionally empty because listener will never be called.
|
||||
}
|
||||
|
||||
@override
|
||||
final RoutingConfig value;
|
||||
}
|
||||
|
@ -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: 11.1.4
|
||||
version: 12.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
|
||||
|
||||
|
@ -11,7 +11,7 @@ import 'test_helpers.dart';
|
||||
void main() {
|
||||
group('RouteBuilder', () {
|
||||
testWidgets('Builds GoRoute', (WidgetTester tester) async {
|
||||
final RouteConfiguration config = RouteConfiguration(
|
||||
final RouteConfiguration config = createRouteConfiguration(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
@ -49,7 +49,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Builds ShellRoute', (WidgetTester tester) async {
|
||||
final RouteConfiguration config = RouteConfiguration(
|
||||
final RouteConfiguration config = createRouteConfiguration(
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
builder:
|
||||
@ -94,7 +94,7 @@ void main() {
|
||||
testWidgets('Uses the correct navigatorKey', (WidgetTester tester) async {
|
||||
final GlobalKey<NavigatorState> rootNavigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
final RouteConfiguration config = RouteConfiguration(
|
||||
final RouteConfiguration config = createRouteConfiguration(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
@ -137,7 +137,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final GlobalKey<NavigatorState> shellNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
final RouteConfiguration config = RouteConfiguration(
|
||||
final RouteConfiguration config = createRouteConfiguration(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -198,7 +198,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final GlobalKey<NavigatorState> shellNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
final RouteConfiguration config = RouteConfiguration(
|
||||
final RouteConfiguration config = createRouteConfiguration(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -264,7 +264,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final GlobalKey<NavigatorState> shellNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
final RouteConfiguration config = RouteConfiguration(
|
||||
final RouteConfiguration config = createRouteConfiguration(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
|
@ -20,7 +20,7 @@ void main() {
|
||||
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
@ -67,7 +67,7 @@ void main() {
|
||||
final List<RouteBase> shellRouteChildren = <RouteBase>[];
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(routes: shellRouteChildren),
|
||||
@ -94,7 +94,7 @@ void main() {
|
||||
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
|
||||
@ -120,7 +120,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
},
|
||||
throwsA(isA<GoError>()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
@ -132,7 +132,7 @@ void main() {
|
||||
final GlobalKey<NavigatorState> keyA =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'A');
|
||||
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
|
||||
@ -168,7 +168,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>();
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
|
||||
@ -218,7 +218,7 @@ void main() {
|
||||
];
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
|
||||
@ -254,7 +254,7 @@ void main() {
|
||||
parentNavigatorKey: sectionANavigatorKey);
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
|
||||
@ -287,7 +287,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>();
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
|
||||
@ -318,7 +318,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
},
|
||||
throwsA(isA<GoError>()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
@ -333,7 +333,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>();
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
|
||||
@ -373,7 +373,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
},
|
||||
throwsA(isA<GoError>()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
});
|
||||
|
||||
@ -383,7 +383,7 @@ void main() {
|
||||
final GlobalKey<NavigatorState> root =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
|
||||
@ -480,7 +480,7 @@ void main() {
|
||||
final StatefulShellBranch branchY;
|
||||
final StatefulShellBranch branchB;
|
||||
|
||||
final RouteConfiguration config = RouteConfiguration(
|
||||
final RouteConfiguration config = createRouteConfiguration(
|
||||
navigatorKey: GlobalKey<NavigatorState>(debugLabel: 'root'),
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(
|
||||
@ -568,7 +568,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -608,7 +608,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
final GlobalKey<NavigatorState> shell2 =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell2');
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -661,7 +661,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final GlobalKey<NavigatorState> shell =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -711,7 +711,7 @@ void main() {
|
||||
final GlobalKey<NavigatorState> shell =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
expect(
|
||||
() => RouteConfiguration(
|
||||
() => createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -749,7 +749,7 @@ void main() {
|
||||
return null;
|
||||
},
|
||||
),
|
||||
throwsA(isA<GoError>()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -761,7 +761,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final GlobalKey<NavigatorState> shell =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -814,7 +814,7 @@ void main() {
|
||||
final GlobalKey<NavigatorState> shell2 =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell2');
|
||||
expect(
|
||||
() => RouteConfiguration(
|
||||
() => createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -857,7 +857,7 @@ void main() {
|
||||
return null;
|
||||
},
|
||||
),
|
||||
throwsA(isA<GoError>()),
|
||||
throwsA(isA<AssertionError>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -865,7 +865,7 @@ void main() {
|
||||
() {
|
||||
final GlobalKey<NavigatorState> root =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
builder: _mockShellBuilder,
|
||||
@ -907,7 +907,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
final GlobalKey<NavigatorState> shell2 =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell2');
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -959,7 +959,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
expect(
|
||||
() {
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
@ -991,7 +991,7 @@ void main() {
|
||||
GlobalKey<NavigatorState>(debugLabel: 'shell');
|
||||
|
||||
expect(
|
||||
RouteConfiguration(
|
||||
createRouteConfiguration(
|
||||
navigatorKey: root,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
|
@ -7,6 +7,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('updateShouldNotify', () {
|
||||
test('does not update when goRouter does not change', () {
|
||||
@ -124,7 +126,10 @@ class _MyWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class MockGoRouter extends GoRouter {
|
||||
MockGoRouter() : super(routes: <GoRoute>[]);
|
||||
MockGoRouter()
|
||||
: super.routingConfig(
|
||||
routingConfig: const ConstantRoutingConfig(
|
||||
RoutingConfig(routes: <RouteBase>[])));
|
||||
|
||||
late String latestPushedName;
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@ -18,6 +20,7 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: '/users/123',
|
||||
matchedLocation: '',
|
||||
matchedPath: '',
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
if (match == null) {
|
||||
@ -39,6 +42,7 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: 'users/123',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
if (match == null) {
|
||||
@ -65,6 +69,7 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: 'users/123',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
if (match == null) {
|
||||
@ -88,6 +93,7 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: 'users/123',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
|
||||
@ -95,6 +101,7 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: 'users/1234',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
|
||||
@ -111,6 +118,7 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: 'users/123',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
|
||||
@ -118,12 +126,73 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: 'users/1234',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
|
||||
expect(match1!.pageKey, match2!.pageKey);
|
||||
});
|
||||
});
|
||||
|
||||
group('ImperativeRouteMatch', () {
|
||||
final RouteMatchList matchList1 = RouteMatchList(
|
||||
matches: <RouteMatch>[
|
||||
RouteMatch(
|
||||
route: GoRoute(path: '/', builder: (_, __) => const Text('hi')),
|
||||
matchedLocation: '/',
|
||||
pageKey: const ValueKey<String>('dummy'),
|
||||
),
|
||||
],
|
||||
uri: Uri.parse('/'),
|
||||
pathParameters: const <String, String>{});
|
||||
|
||||
final RouteMatchList matchList2 = RouteMatchList(
|
||||
matches: <RouteMatch>[
|
||||
RouteMatch(
|
||||
route: GoRoute(path: '/a', builder: (_, __) => const Text('a')),
|
||||
matchedLocation: '/a',
|
||||
pageKey: const ValueKey<String>('dummy'),
|
||||
),
|
||||
],
|
||||
uri: Uri.parse('/a'),
|
||||
pathParameters: const <String, String>{});
|
||||
|
||||
const ValueKey<String> key1 = ValueKey<String>('key1');
|
||||
const ValueKey<String> key2 = ValueKey<String>('key2');
|
||||
|
||||
final Completer<void> completer1 = Completer<void>();
|
||||
final Completer<void> completer2 = Completer<void>();
|
||||
|
||||
test('can equal and has', () async {
|
||||
ImperativeRouteMatch match1 = ImperativeRouteMatch(
|
||||
pageKey: key1, matches: matchList1, completer: completer1);
|
||||
ImperativeRouteMatch match2 = ImperativeRouteMatch(
|
||||
pageKey: key1, matches: matchList1, completer: completer1);
|
||||
expect(match1 == match2, isTrue);
|
||||
expect(match1.hashCode == match2.hashCode, isTrue);
|
||||
|
||||
match1 = ImperativeRouteMatch(
|
||||
pageKey: key1, matches: matchList1, completer: completer1);
|
||||
match2 = ImperativeRouteMatch(
|
||||
pageKey: key2, matches: matchList1, completer: completer1);
|
||||
expect(match1 == match2, isFalse);
|
||||
expect(match1.hashCode == match2.hashCode, isFalse);
|
||||
|
||||
match1 = ImperativeRouteMatch(
|
||||
pageKey: key1, matches: matchList1, completer: completer1);
|
||||
match2 = ImperativeRouteMatch(
|
||||
pageKey: key1, matches: matchList2, completer: completer1);
|
||||
expect(match1 == match2, isFalse);
|
||||
expect(match1.hashCode == match2.hashCode, isFalse);
|
||||
|
||||
match1 = ImperativeRouteMatch(
|
||||
pageKey: key1, matches: matchList1, completer: completer1);
|
||||
match2 = ImperativeRouteMatch(
|
||||
pageKey: key1, matches: matchList1, completer: completer2);
|
||||
expect(match1 == match2, isFalse);
|
||||
expect(match1.hashCode == match2.hashCode, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _builder(BuildContext context, GoRouterState state) =>
|
||||
|
@ -39,6 +39,7 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: '/page-0',
|
||||
matchedLocation: '',
|
||||
matchedPath: '',
|
||||
pathParameters: params1,
|
||||
)!;
|
||||
|
||||
@ -47,6 +48,7 @@ void main() {
|
||||
route: route,
|
||||
remainingLocation: '/page-0',
|
||||
matchedLocation: '',
|
||||
matchedPath: '',
|
||||
pathParameters: params2,
|
||||
)!;
|
||||
|
||||
@ -73,7 +75,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('RouteMatchList is encoded and decoded correctly', () {
|
||||
final RouteConfiguration configuration = RouteConfiguration(
|
||||
final RouteConfiguration configuration = createRouteConfiguration(
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/a',
|
||||
|
@ -155,7 +155,7 @@ void main() {
|
||||
),
|
||||
];
|
||||
|
||||
final RouteConfiguration configuration = RouteConfiguration(
|
||||
final RouteConfiguration configuration = createRouteConfiguration(
|
||||
routes: routes,
|
||||
redirectLimit: 100,
|
||||
topRedirect: (_, __) => null,
|
||||
@ -195,7 +195,7 @@ void main() {
|
||||
),
|
||||
];
|
||||
|
||||
final RouteConfiguration configuration = RouteConfiguration(
|
||||
final RouteConfiguration configuration = createRouteConfiguration(
|
||||
routes: routes,
|
||||
redirectLimit: 100,
|
||||
topRedirect: (_, __) => null,
|
||||
|
109
packages/go_router/test/routing_config_test.dart
Normal file
109
packages/go_router/test/routing_config_test.dart
Normal file
@ -0,0 +1,109 @@
|
||||
// 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:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('routing config works', (WidgetTester tester) async {
|
||||
final ValueNotifier<RoutingConfig> config = ValueNotifier<RoutingConfig>(
|
||||
RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(path: '/', builder: (_, __) => const Text('home')),
|
||||
],
|
||||
redirect: (_, __) => '/',
|
||||
),
|
||||
);
|
||||
final GoRouter router = await createRouterWithRoutingConfig(config, tester);
|
||||
expect(find.text('home'), findsOneWidget);
|
||||
|
||||
router.go('/abcd'); // should be redirected to home
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('home'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('routing config works after builder changes',
|
||||
(WidgetTester tester) async {
|
||||
final ValueNotifier<RoutingConfig> config = ValueNotifier<RoutingConfig>(
|
||||
RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(path: '/', builder: (_, __) => const Text('home')),
|
||||
],
|
||||
),
|
||||
);
|
||||
await createRouterWithRoutingConfig(config, tester);
|
||||
expect(find.text('home'), findsOneWidget);
|
||||
|
||||
config.value = RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(path: '/', builder: (_, __) => const Text('home1')),
|
||||
],
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('home1'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('routing config works after routing changes',
|
||||
(WidgetTester tester) async {
|
||||
final ValueNotifier<RoutingConfig> config = ValueNotifier<RoutingConfig>(
|
||||
RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(path: '/', builder: (_, __) => const Text('home')),
|
||||
],
|
||||
),
|
||||
);
|
||||
final GoRouter router = await createRouterWithRoutingConfig(
|
||||
config,
|
||||
tester,
|
||||
errorBuilder: (_, __) => const Text('error'),
|
||||
);
|
||||
expect(find.text('home'), findsOneWidget);
|
||||
// Sanity check.
|
||||
router.go('/abc');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('error'), findsOneWidget);
|
||||
|
||||
config.value = RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(path: '/', builder: (_, __) => const Text('home')),
|
||||
GoRoute(path: '/abc', builder: (_, __) => const Text('/abc')),
|
||||
],
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('/abc'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('routing config works after routing changes case 2',
|
||||
(WidgetTester tester) async {
|
||||
final ValueNotifier<RoutingConfig> config = ValueNotifier<RoutingConfig>(
|
||||
RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(path: '/', builder: (_, __) => const Text('home')),
|
||||
GoRoute(path: '/abc', builder: (_, __) => const Text('/abc')),
|
||||
],
|
||||
),
|
||||
);
|
||||
final GoRouter router = await createRouterWithRoutingConfig(
|
||||
config,
|
||||
tester,
|
||||
errorBuilder: (_, __) => const Text('error'),
|
||||
);
|
||||
expect(find.text('home'), findsOneWidget);
|
||||
// Sanity check.
|
||||
router.go('/abc');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('/abc'), findsOneWidget);
|
||||
|
||||
config.value = RoutingConfig(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(path: '/', builder: (_, __) => const Text('home')),
|
||||
],
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('error'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
|
||||
// ignore_for_file: cascade_invocations, diagnostic_describe_all_properties
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -34,7 +35,10 @@ Widget fakeNavigationBuilder(
|
||||
child;
|
||||
|
||||
class GoRouterNamedLocationSpy extends GoRouter {
|
||||
GoRouterNamedLocationSpy({required super.routes});
|
||||
GoRouterNamedLocationSpy({required List<RouteBase> routes})
|
||||
: super.routingConfig(
|
||||
routingConfig:
|
||||
ConstantRoutingConfig(RoutingConfig(routes: routes)));
|
||||
|
||||
String? name;
|
||||
Map<String, String>? pathParameters;
|
||||
@ -54,7 +58,10 @@ class GoRouterNamedLocationSpy extends GoRouter {
|
||||
}
|
||||
|
||||
class GoRouterGoSpy extends GoRouter {
|
||||
GoRouterGoSpy({required super.routes});
|
||||
GoRouterGoSpy({required List<RouteBase> routes})
|
||||
: super.routingConfig(
|
||||
routingConfig:
|
||||
ConstantRoutingConfig(RoutingConfig(routes: routes)));
|
||||
|
||||
String? myLocation;
|
||||
Object? extra;
|
||||
@ -67,7 +74,10 @@ class GoRouterGoSpy extends GoRouter {
|
||||
}
|
||||
|
||||
class GoRouterGoNamedSpy extends GoRouter {
|
||||
GoRouterGoNamedSpy({required super.routes});
|
||||
GoRouterGoNamedSpy({required List<RouteBase> routes})
|
||||
: super.routingConfig(
|
||||
routingConfig:
|
||||
ConstantRoutingConfig(RoutingConfig(routes: routes)));
|
||||
|
||||
String? name;
|
||||
Map<String, String>? pathParameters;
|
||||
@ -89,7 +99,10 @@ class GoRouterGoNamedSpy extends GoRouter {
|
||||
}
|
||||
|
||||
class GoRouterPushSpy extends GoRouter {
|
||||
GoRouterPushSpy({required super.routes});
|
||||
GoRouterPushSpy({required List<RouteBase> routes})
|
||||
: super.routingConfig(
|
||||
routingConfig:
|
||||
ConstantRoutingConfig(RoutingConfig(routes: routes)));
|
||||
|
||||
String? myLocation;
|
||||
Object? extra;
|
||||
@ -103,7 +116,10 @@ class GoRouterPushSpy extends GoRouter {
|
||||
}
|
||||
|
||||
class GoRouterPushNamedSpy extends GoRouter {
|
||||
GoRouterPushNamedSpy({required super.routes});
|
||||
GoRouterPushNamedSpy({required List<RouteBase> routes})
|
||||
: super.routingConfig(
|
||||
routingConfig:
|
||||
ConstantRoutingConfig(RoutingConfig(routes: routes)));
|
||||
|
||||
String? name;
|
||||
Map<String, String>? pathParameters;
|
||||
@ -126,7 +142,10 @@ class GoRouterPushNamedSpy extends GoRouter {
|
||||
}
|
||||
|
||||
class GoRouterPopSpy extends GoRouter {
|
||||
GoRouterPopSpy({required super.routes});
|
||||
GoRouterPopSpy({required List<RouteBase> routes})
|
||||
: super.routingConfig(
|
||||
routingConfig:
|
||||
ConstantRoutingConfig(RoutingConfig(routes: routes)));
|
||||
|
||||
bool popped = false;
|
||||
Object? poppedResult;
|
||||
@ -175,6 +194,39 @@ Future<GoRouter> createRouter(
|
||||
return goRouter;
|
||||
}
|
||||
|
||||
Future<GoRouter> createRouterWithRoutingConfig(
|
||||
ValueListenable<RoutingConfig> config,
|
||||
WidgetTester tester, {
|
||||
String initialLocation = '/',
|
||||
Object? initialExtra,
|
||||
GlobalKey<NavigatorState>? navigatorKey,
|
||||
GoRouterWidgetBuilder? errorBuilder,
|
||||
String? restorationScopeId,
|
||||
GoExceptionHandler? onException,
|
||||
bool requestFocus = true,
|
||||
bool overridePlatformDefaultLocation = false,
|
||||
}) async {
|
||||
final GoRouter goRouter = GoRouter.routingConfig(
|
||||
routingConfig: config,
|
||||
initialLocation: initialLocation,
|
||||
onException: onException,
|
||||
initialExtra: initialExtra,
|
||||
errorBuilder: errorBuilder,
|
||||
navigatorKey: navigatorKey,
|
||||
restorationScopeId: restorationScopeId,
|
||||
requestFocus: requestFocus,
|
||||
overridePlatformDefaultLocation: overridePlatformDefaultLocation,
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp.router(
|
||||
restorationScopeId:
|
||||
restorationScopeId != null ? '$restorationScopeId-root' : null,
|
||||
routerConfig: goRouter,
|
||||
),
|
||||
);
|
||||
return goRouter;
|
||||
}
|
||||
|
||||
class TestErrorScreen extends DummyScreen {
|
||||
const TestErrorScreen(this.ex, {super.key});
|
||||
|
||||
@ -307,3 +359,35 @@ RouteMatch createRouteMatch(RouteBase route, String location) {
|
||||
pageKey: ValueKey<String>(location),
|
||||
);
|
||||
}
|
||||
|
||||
/// A routing config that is never going to change.
|
||||
class ConstantRoutingConfig extends ValueListenable<RoutingConfig> {
|
||||
const ConstantRoutingConfig(this.value);
|
||||
@override
|
||||
void addListener(VoidCallback listener) {
|
||||
// Intentionally empty because listener will never be called.
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(VoidCallback listener) {
|
||||
// Intentionally empty because listener will never be called.
|
||||
}
|
||||
|
||||
@override
|
||||
final RoutingConfig value;
|
||||
}
|
||||
|
||||
RouteConfiguration createRouteConfiguration({
|
||||
required List<RouteBase> routes,
|
||||
required GlobalKey<NavigatorState> navigatorKey,
|
||||
required GoRouterRedirect topRedirect,
|
||||
required int redirectLimit,
|
||||
}) {
|
||||
return RouteConfiguration(
|
||||
ConstantRoutingConfig(RoutingConfig(
|
||||
routes: routes,
|
||||
redirect: topRedirect,
|
||||
redirectLimit: redirectLimit,
|
||||
)),
|
||||
navigatorKey: navigatorKey);
|
||||
}
|
||||
|
Reference in New Issue
Block a user