[go_router] Adds parent navigator key to ShellRoute and StatefulShell… (#4201)

…Route.

fixes https://github.com/flutter/flutter/issues/111678
fixes https://github.com/flutter/flutter/issues/128793
This commit is contained in:
chunhtai
2023-06-21 15:10:00 -07:00
committed by GitHub
parent cdc1574312
commit b321f2c0ac
7 changed files with 318 additions and 117 deletions

View File

@ -1,3 +1,7 @@
## 8.1.0
- Adds parent navigator key to ShellRoute and StatefulShellRoute.
## 8.0.5
- Fixes a bug that GoRouterState in top level redirect doesn't contain complete data.

View File

@ -204,77 +204,73 @@ class RouteBuilder {
keyToPages.putIfAbsent(navigatorKey, () => <Page<Object?>>[]).add(page);
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
routerNeglect, keyToPages, navigatorKey, registry);
} else if (route is GoRoute) {
page = _buildPageForGoRoute(context, state, match, route, pagePopContext);
// If this GoRoute is for a different Navigator, add it to the
} else {
// If this RouteBase is for a different Navigator, add it to the
// list of out of scope pages
final GlobalKey<NavigatorState> goRouteNavKey =
final GlobalKey<NavigatorState> routeNavKey =
route.parentNavigatorKey ?? navigatorKey;
if (route is GoRoute) {
page =
_buildPageForGoRoute(context, state, match, route, pagePopContext);
keyToPages.putIfAbsent(goRouteNavKey, () => <Page<Object?>>[]).add(page);
keyToPages.putIfAbsent(routeNavKey, () => <Page<Object?>>[]).add(page);
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
routerNeglect, keyToPages, navigatorKey, registry);
} else if (route is ShellRouteBase) {
assert(startIndex + 1 < matchList.matches.length,
'Shell routes must always have child routes');
// The key for the Navigator that will display this ShellRoute's page.
final GlobalKey<NavigatorState> parentNavigatorKey = navigatorKey;
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
routerNeglect, keyToPages, navigatorKey, registry);
} else if (route is ShellRouteBase) {
assert(startIndex + 1 < matchList.matches.length,
'Shell routes must always have child routes');
// Add an entry for the parent navigator if none exists.
keyToPages.putIfAbsent(parentNavigatorKey, () => <Page<Object?>>[]);
// Add an entry for the parent navigator if none exists.
//
// Calling _buildRecursive can result in adding pages to the
// parentNavigatorKey entry's list. Store the current length so
// that the page for this ShellRoute is placed at the right index.
final int shellPageIdx =
keyToPages.putIfAbsent(routeNavKey, () => <Page<Object?>>[]).length;
// Calling _buildRecursive can result in adding pages to the
// parentNavigatorKey entry's list. Store the current length so
// that the page for this ShellRoute is placed at the right index.
final int shellPageIdx = keyToPages[parentNavigatorKey]!.length;
// Find the the navigator key for the sub-route of this shell route.
final RouteBase subRoute = matchList.matches[startIndex + 1].route;
final GlobalKey<NavigatorState> shellNavigatorKey =
route.navigatorKeyForSubRoute(subRoute);
// Get the current sub-route of this shell route from the match list.
final RouteBase subRoute = matchList.matches[startIndex + 1].route;
keyToPages.putIfAbsent(shellNavigatorKey, () => <Page<Object?>>[]);
// The key to provide to the shell route's Navigator.
final GlobalKey<NavigatorState> shellNavigatorKey =
route.navigatorKeyForSubRoute(subRoute);
// Build the remaining pages
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
routerNeglect, keyToPages, shellNavigatorKey, registry);
// Add an entry for the shell route's navigator
keyToPages.putIfAbsent(shellNavigatorKey, () => <Page<Object?>>[]);
final HeroController heroController = _goHeroCache.putIfAbsent(
shellNavigatorKey, () => _getHeroController(context));
// Build the remaining pages
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
routerNeglect, keyToPages, shellNavigatorKey, registry);
// Build the Navigator for this shell route
Widget buildShellNavigator(
List<NavigatorObserver>? observers, String? restorationScopeId) {
return _buildNavigator(
pagePopContext.onPopPage,
keyToPages[shellNavigatorKey]!,
shellNavigatorKey,
observers: observers ?? const <NavigatorObserver>[],
restorationScopeId: restorationScopeId,
heroController: heroController,
);
}
final HeroController heroController = _goHeroCache.putIfAbsent(
shellNavigatorKey, () => _getHeroController(context));
// Build the Navigator for this shell route
Widget buildShellNavigator(
List<NavigatorObserver>? observers, String? restorationScopeId) {
return _buildNavigator(
pagePopContext.onPopPage,
keyToPages[shellNavigatorKey]!,
shellNavigatorKey,
observers: observers ?? const <NavigatorObserver>[],
restorationScopeId: restorationScopeId,
heroController: heroController,
// Call the ShellRouteBase to create/update the shell route state
final ShellRouteContext shellRouteContext = ShellRouteContext(
route: route,
routerState: state,
navigatorKey: shellNavigatorKey,
routeMatchList: matchList,
navigatorBuilder: buildShellNavigator,
);
// Build the Page for this route
page = _buildPageForShellRoute(
context, state, match, route, pagePopContext, shellRouteContext);
// Place the ShellRoute's Page onto the list for the parent navigator.
keyToPages[routeNavKey]!.insert(shellPageIdx, page);
}
// Call the ShellRouteBase to create/update the shell route state
final ShellRouteContext shellRouteContext = ShellRouteContext(
route: route,
routerState: state,
navigatorKey: shellNavigatorKey,
routeMatchList: matchList,
navigatorBuilder: buildShellNavigator,
);
// Build the Page for this route
page = _buildPageForShellRoute(
context, state, match, route, pagePopContext, shellRouteContext);
// Place the ShellRoute's Page onto the list for the parent navigator.
keyToPages
.putIfAbsent(parentNavigatorKey, () => <Page<Object?>>[])
.insert(shellPageIdx, page);
}
if (page != null) {
registry[page] = state;

View File

@ -148,73 +148,61 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
/// pageless route, such as a dialog or bottom sheet.
class _NavigatorStateIterator extends Iterator<NavigatorState> {
_NavigatorStateIterator(this.matchList, this.root)
: index = matchList.matches.length;
: index = matchList.matches.length - 1;
final RouteMatchList matchList;
int index = 0;
int index;
final NavigatorState root;
@override
late NavigatorState current;
RouteBase _getRouteAtIndex(int index) => matchList.matches[index].route;
void _findsNextIndex() {
final GlobalKey<NavigatorState>? parentNavigatorKey =
_getRouteAtIndex(index).parentNavigatorKey;
if (parentNavigatorKey == null) {
index -= 1;
return;
}
for (index -= 1; index >= 0; index -= 1) {
final RouteBase route = _getRouteAtIndex(index);
if (route is ShellRouteBase) {
if (route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1)) ==
parentNavigatorKey) {
return;
}
}
}
assert(root == parentNavigatorKey.currentState);
}
@override
bool moveNext() {
if (index < 0) {
return false;
}
late RouteBase subRoute;
for (index -= 1; index >= 0; index -= 1) {
final RouteMatch match = matchList.matches[index];
final RouteBase route = match.route;
if (route is GoRoute && route.parentNavigatorKey != null) {
final GlobalKey<NavigatorState> parentNavigatorKey =
route.parentNavigatorKey!;
final ModalRoute<Object?>? parentModalRoute =
ModalRoute.of(parentNavigatorKey.currentContext!);
// The ModalRoute can be null if the parentNavigatorKey references the
// root navigator.
if (parentModalRoute == null) {
index = -1;
assert(root == parentNavigatorKey.currentState);
current = root;
return true;
}
// It must be a ShellRoute that holds this parentNavigatorKey;
// otherwise, parentModalRoute would have been null. Updates the index
// to the ShellRoute
for (index -= 1; index >= 0; index -= 1) {
final RouteBase route = matchList.matches[index].route;
if (route is ShellRoute) {
if (route.navigatorKey == parentNavigatorKey) {
break;
}
}
}
// There may be a pageless route on top of ModalRoute that the
// NavigatorState of parentNavigatorKey is in. For example, an open
// dialog. In that case we want to find the navigator that host the
// pageless route.
if (parentModalRoute.isCurrent == false) {
continue;
}
_findsNextIndex();
current = parentNavigatorKey.currentState!;
return true;
} else if (route is ShellRouteBase) {
while (index >= 0) {
final RouteBase route = _getRouteAtIndex(index);
if (route is ShellRouteBase) {
final GlobalKey<NavigatorState> navigatorKey =
route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1));
// Must have a ModalRoute parent because the navigator ShellRoute
// created must not be the root navigator.
final GlobalKey<NavigatorState> navigatorKey =
route.navigatorKeyForSubRoute(subRoute);
final ModalRoute<Object?> parentModalRoute =
ModalRoute.of(navigatorKey.currentContext!)!;
// There may be pageless route on top of ModalRoute that the
// parentNavigatorKey is in. For example an open dialog.
if (parentModalRoute.isCurrent == false) {
continue;
if (parentModalRoute.isCurrent) {
current = navigatorKey.currentState!;
return true;
}
current = navigatorKey.currentState!;
return true;
}
subRoute = route;
_findsNextIndex();
}
assert(index == -1);
current = root;

View File

@ -98,12 +98,20 @@ import 'typedefs.dart';
@immutable
abstract class RouteBase {
const RouteBase._({
this.routes = const <RouteBase>[],
required this.routes,
required this.parentNavigatorKey,
});
/// The list of child routes associated with this route.
final List<RouteBase> routes;
/// An optional key specifying which Navigator to display this route's screen
/// onto.
///
/// Specifying the root Navigator will stack this route onto that
/// Navigator instead of the nearest ShellRoute ancestor.
final GlobalKey<NavigatorState>? parentNavigatorKey;
/// Builds a lists containing the provided routes along with all their
/// descendant [routes].
static Iterable<RouteBase> routesRecursively(Iterable<RouteBase> routes) {
@ -137,7 +145,7 @@ class GoRoute extends RouteBase {
this.name,
this.builder,
this.pageBuilder,
this.parentNavigatorKey,
super.parentNavigatorKey,
this.redirect,
super.routes = const <RouteBase>[],
}) : assert(path.isNotEmpty, 'GoRoute path cannot be empty'),
@ -301,13 +309,6 @@ class GoRoute extends RouteBase {
/// re-evaluation will be triggered if the [InheritedWidget] changes.
final GoRouterRedirect? redirect;
/// An optional key specifying which Navigator to display this route's screen
/// onto.
///
/// Specifying the root Navigator will stack this route onto that
/// Navigator instead of the nearest ShellRoute ancestor.
final GlobalKey<NavigatorState>? parentNavigatorKey;
// TODO(chunhtai): move all regex related help methods to path_utils.dart.
/// Match this route against a location.
RegExpMatch? matchPatternAsPrefix(String loc) =>
@ -333,7 +334,9 @@ class GoRoute extends RouteBase {
/// as [ShellRoute] and [StatefulShellRoute].
abstract class ShellRouteBase extends RouteBase {
/// Constructs a [ShellRouteBase].
const ShellRouteBase._({super.routes}) : super._();
const ShellRouteBase._(
{required super.routes, required super.parentNavigatorKey})
: super._();
/// Attempts to build the Widget representing this shell route.
///
@ -496,7 +499,8 @@ class ShellRoute extends ShellRouteBase {
this.builder,
this.pageBuilder,
this.observers,
super.routes,
required super.routes,
super.parentNavigatorKey,
GlobalKey<NavigatorState>? navigatorKey,
this.restorationScopeId,
}) : assert(routes.isNotEmpty),
@ -653,6 +657,7 @@ class StatefulShellRoute extends ShellRouteBase {
this.builder,
this.pageBuilder,
required this.navigatorContainerBuilder,
super.parentNavigatorKey,
this.restorationScopeId,
}) : assert(branches.isNotEmpty),
assert((pageBuilder != null) ^ (builder != null),
@ -676,12 +681,14 @@ class StatefulShellRoute extends ShellRouteBase {
StatefulShellRoute.indexedStack({
required List<StatefulShellBranch> branches,
StatefulShellRouteBuilder? builder,
GlobalKey<NavigatorState>? parentNavigatorKey,
StatefulShellRoutePageBuilder? pageBuilder,
String? restorationScopeId,
}) : this(
branches: branches,
builder: builder,
pageBuilder: pageBuilder,
parentNavigatorKey: parentNavigatorKey,
restorationScopeId: restorationScopeId,
navigatorContainerBuilder: _indexedStackContainerBuilder,
);

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: 8.0.5
version: 8.1.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

@ -7,6 +7,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/match.dart';
import 'package:go_router/src/misc/error_screen.dart';
import 'package:go_router/src/misc/errors.dart';
import 'test_helpers.dart';
@ -96,6 +97,73 @@ void main() {
await goRouter.routerDelegate.popRoute();
expect(await goRouter.routerDelegate.popRoute(), isFalse);
});
testWidgets('throw if nothing to pop', (WidgetTester tester) async {
final GlobalKey<NavigatorState> rootKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
final GoRouter goRouter = await createRouter(
<RouteBase>[
ShellRoute(
navigatorKey: rootKey,
builder: (_, __, Widget child) => child,
routes: <RouteBase>[
ShellRoute(
parentNavigatorKey: rootKey,
navigatorKey: navKey,
builder: (_, __, Widget child) => child,
routes: <RouteBase>[
GoRoute(
path: '/',
parentNavigatorKey: navKey,
builder: (_, __) => const Text('Home'),
),
],
),
],
),
],
tester,
);
await tester.pumpAndSettle();
expect(find.text('Home'), findsOneWidget);
String? message;
try {
goRouter.pop();
} on GoError catch (e) {
message = e.message;
}
expect(message, 'There is nothing to pop');
});
testWidgets('poproute return false if nothing to pop',
(WidgetTester tester) async {
final GlobalKey<NavigatorState> rootKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
final GoRouter goRouter = await createRouter(
<RouteBase>[
ShellRoute(
navigatorKey: rootKey,
builder: (_, __, Widget child) => child,
routes: <RouteBase>[
ShellRoute(
parentNavigatorKey: rootKey,
navigatorKey: navKey,
builder: (_, __, Widget child) => child,
routes: <RouteBase>[
GoRoute(
path: '/',
parentNavigatorKey: navKey,
builder: (_, __) => const Text('Home'),
),
],
),
],
),
],
tester,
);
expect(await goRouter.routerDelegate.popRoute(), isFalse);
});
});
group('push', () {

View File

@ -2,9 +2,12 @@
// 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() {
test('throws when a builder is not set', () {
expect(() => GoRoute(path: '/'), throwsA(isAssertionError));
@ -17,4 +20,139 @@ void main() {
test('does not throw when only redirect is provided', () {
GoRoute(path: '/', redirect: (_, __) => '/a');
});
testWidgets('ShellRoute can use parent navigator key',
(WidgetTester tester) async {
final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> shellNavigatorKey =
GlobalKey<NavigatorState>();
final List<RouteBase> routes = <RouteBase>[
ShellRoute(
navigatorKey: shellNavigatorKey,
builder: (BuildContext context, GoRouterState state, Widget child) {
return Scaffold(
body: Column(
children: <Widget>[
const Text('Screen A'),
Expanded(child: child),
],
),
);
},
routes: <RouteBase>[
GoRoute(
path: '/b',
builder: (BuildContext context, GoRouterState state) {
return const Scaffold(
body: Text('Screen B'),
);
},
routes: <RouteBase>[
ShellRoute(
parentNavigatorKey: rootNavigatorKey,
builder:
(BuildContext context, GoRouterState state, Widget child) {
return Scaffold(
body: Column(
children: <Widget>[
const Text('Screen D'),
Expanded(child: child),
],
),
);
},
routes: <RouteBase>[
GoRoute(
path: 'c',
builder: (BuildContext context, GoRouterState state) {
return const Scaffold(
body: Text('Screen C'),
);
},
),
],
),
],
),
],
),
];
await createRouter(routes, tester,
initialLocation: '/b/c', navigatorKey: rootNavigatorKey);
expect(find.text('Screen A'), findsNothing);
expect(find.text('Screen B'), findsNothing);
expect(find.text('Screen D'), findsOneWidget);
expect(find.text('Screen C'), findsOneWidget);
});
testWidgets('StatefulShellRoute can use parent navigator key',
(WidgetTester tester) async {
final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> shellNavigatorKey =
GlobalKey<NavigatorState>();
final List<RouteBase> routes = <RouteBase>[
ShellRoute(
navigatorKey: shellNavigatorKey,
builder: (BuildContext context, GoRouterState state, Widget child) {
return Scaffold(
body: Column(
children: <Widget>[
const Text('Screen A'),
Expanded(child: child),
],
),
);
},
routes: <RouteBase>[
GoRoute(
path: '/b',
builder: (BuildContext context, GoRouterState state) {
return const Scaffold(
body: Text('Screen B'),
);
},
routes: <RouteBase>[
StatefulShellRoute.indexedStack(
parentNavigatorKey: rootNavigatorKey,
builder: (_, __, StatefulNavigationShell navigationShell) {
return Column(
children: <Widget>[
const Text('Screen D'),
Expanded(child: navigationShell),
],
);
},
branches: <StatefulShellBranch>[
StatefulShellBranch(
routes: <RouteBase>[
GoRoute(
path: 'c',
builder: (BuildContext context, GoRouterState state) {
return const Scaffold(
body: Text('Screen C'),
);
},
),
],
),
],
),
],
),
],
),
];
await createRouter(routes, tester,
initialLocation: '/b/c', navigatorKey: rootNavigatorKey);
expect(find.text('Screen A'), findsNothing);
expect(find.text('Screen B'), findsNothing);
expect(find.text('Screen D'), findsOneWidget);
expect(find.text('Screen C'), findsOneWidget);
});
}