[go_router] adds ability to dynamically update routing table. (#5079)

fixes https://github.com/flutter/flutter/issues/136005
This commit is contained in:
chunhtai
2023-10-18 16:19:22 -07:00
committed by GitHub
parent 47a14fd229
commit 1a24dadf92
18 changed files with 711 additions and 227 deletions

View File

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

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

View File

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

View 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,
);
}
}

View 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);
});
}

View File

@ -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:');

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
});
}

View File

@ -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);
}