Skip to content

Commit 38f1715

Browse files
authored
[go_router] Allows redirect only GoRoute to be part of RouteMatchList (flutter#4315)
fixes flutter#114807
1 parent 8f6e326 commit 38f1715

File tree

9 files changed

+226
-42
lines changed

9 files changed

+226
-42
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 9.0.1
2+
3+
- Allows redirect only GoRoute to be part of RouteMatchList.
4+
15
## 9.0.0
26

37
- **BREAKING CHANGE**:

packages/go_router/lib/src/builder.dart

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ class RouteBuilder {
8888
// empty box until then.
8989
return const SizedBox.shrink();
9090
}
91+
assert(
92+
matchList.isError || !(matchList.last.route as GoRoute).redirectOnly);
9193
return builderWithNav(
9294
context,
9395
Builder(
@@ -212,8 +214,12 @@ class RouteBuilder {
212214
if (route is GoRoute) {
213215
page =
214216
_buildPageForGoRoute(context, state, match, route, pagePopContext);
215-
216-
keyToPages.putIfAbsent(routeNavKey, () => <Page<Object?>>[]).add(page);
217+
assert(page != null || route.redirectOnly);
218+
if (page != null) {
219+
keyToPages
220+
.putIfAbsent(routeNavKey, () => <Page<Object?>>[])
221+
.add(page);
222+
}
217223

218224
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
219225
routerNeglect, keyToPages, navigatorKey, registry);
@@ -275,8 +281,6 @@ class RouteBuilder {
275281
if (page != null) {
276282
registry[page] = state;
277283
pagePopContext._setRouteMatchForPage(page, match);
278-
} else {
279-
throw GoError('Unsupported route type $route');
280284
}
281285
}
282286

@@ -344,36 +348,30 @@ class RouteBuilder {
344348
}
345349

346350
/// Builds a [Page] for [GoRoute]
347-
Page<Object?> _buildPageForGoRoute(BuildContext context, GoRouterState state,
351+
Page<Object?>? _buildPageForGoRoute(BuildContext context, GoRouterState state,
348352
RouteMatch match, GoRoute route, _PagePopContext pagePopContext) {
349-
Page<Object?>? page;
350-
351353
// Call the pageBuilder if it's non-null
352354
final GoRouterPageBuilder? pageBuilder = route.pageBuilder;
353355
if (pageBuilder != null) {
354-
page = pageBuilder(context, state);
355-
if (page is NoOpPage) {
356-
page = null;
356+
final Page<Object?> page = pageBuilder(context, state);
357+
if (page is! NoOpPage) {
358+
return page;
357359
}
358360
}
359-
360-
// Return the result of the route's builder() or pageBuilder()
361-
return page ??
362-
buildPage(context, state, Builder(builder: (BuildContext context) {
363-
return _callGoRouteBuilder(context, state, route);
364-
}));
361+
return _callGoRouteBuilder(context, state, route);
365362
}
366363

367364
/// Calls the user-provided route builder from the [GoRoute].
368-
Widget _callGoRouteBuilder(
365+
Page<Object?>? _callGoRouteBuilder(
369366
BuildContext context, GoRouterState state, GoRoute route) {
370367
final GoRouterWidgetBuilder? builder = route.builder;
371368

372369
if (builder == null) {
373-
throw GoError('No routeBuilder provided to GoRoute: $route');
370+
return null;
374371
}
375-
376-
return builder(context, state);
372+
return buildPage(context, state, Builder(builder: (BuildContext context) {
373+
return builder(context, state);
374+
}));
377375
}
378376

379377
/// Builds a [Page] for [ShellRouteBase]

packages/go_router/lib/src/match.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,11 @@ class RouteMatchList {
263263
assert(index != -1);
264264
newMatches.removeRange(index, newMatches.length);
265265

266-
// Also pop ShellRoutes when there are no subsequent route matches
267-
while (newMatches.isNotEmpty && newMatches.last.route is ShellRouteBase) {
266+
// Also pop ShellRoutes that have no subsequent route matches and GoRoutes
267+
// that only have redirect.
268+
while (newMatches.isNotEmpty &&
269+
(newMatches.last.route is ShellRouteBase ||
270+
(newMatches.last.route as GoRoute).redirectOnly)) {
268271
newMatches.removeLast();
269272
}
270273
// Removing ImperativeRouteMatch should not change uri and pathParameters.

packages/go_router/lib/src/parser.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
9898
if (matchList.isError && onParserException != null) {
9999
return onParserException!(context, matchList);
100100
}
101+
102+
assert(() {
103+
if (matchList.isNotEmpty) {
104+
assert(!(matchList.last.route as GoRoute).redirectOnly,
105+
'A redirect-only route must redirect to location different from itself.\n The offending route: ${matchList.last.route}');
106+
}
107+
return true;
108+
}());
101109
return _updateRouteMatchList(
102110
matchList,
103111
baseRouteMatchList: state.baseRouteMatchList,

packages/go_router/lib/src/route.dart

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import 'package:collection/collection.dart';
6+
import 'package:flutter/foundation.dart';
67
import 'package:flutter/widgets.dart';
78
import 'package:meta/meta.dart';
89

@@ -96,7 +97,7 @@ import 'typedefs.dart';
9697
/// ///
9798
/// See [main.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/main.dart)
9899
@immutable
99-
abstract class RouteBase {
100+
abstract class RouteBase with Diagnosticable {
100101
const RouteBase._({
101102
required this.routes,
102103
required this.parentNavigatorKey,
@@ -118,6 +119,15 @@ abstract class RouteBase {
118119
return routes.expand(
119120
(RouteBase e) => <RouteBase>[e, ...routesRecursively(e.routes)]);
120121
}
122+
123+
@override
124+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
125+
super.debugFillProperties(properties);
126+
if (parentNavigatorKey != null) {
127+
properties.add(DiagnosticsProperty<GlobalKey<NavigatorState>>(
128+
'parentNavKey', parentNavigatorKey));
129+
}
130+
}
121131
}
122132

123133
/// A route that is displayed visually above the matching parent route using the
@@ -157,6 +167,11 @@ class GoRoute extends RouteBase {
157167
_pathRE = patternToRegExp(path, pathParameters);
158168
}
159169

170+
/// Whether this [GoRoute] only redirects to another route.
171+
///
172+
/// If this is true, this route must redirect location other than itself.
173+
bool get redirectOnly => pageBuilder == null && builder == null;
174+
160175
/// Optional name of the route.
161176
///
162177
/// If used, a unique string name must be provided and it can not be empty.
@@ -323,8 +338,12 @@ class GoRoute extends RouteBase {
323338
final List<String> pathParameters = <String>[];
324339

325340
@override
326-
String toString() {
327-
return 'GoRoute(name: $name, path: $path)';
341+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
342+
super.debugFillProperties(properties);
343+
properties.add(StringProperty('name', name));
344+
properties.add(StringProperty('path', path));
345+
properties.add(
346+
FlagProperty('redirect', value: redirectOnly, ifTrue: 'Redirect Only'));
328347
}
329348

330349
late final RegExp _pathRE;
@@ -338,6 +357,21 @@ abstract class ShellRouteBase extends RouteBase {
338357
{required super.routes, required super.parentNavigatorKey})
339358
: super._();
340359

360+
static void _debugCheckSubRouteParentNavigatorKeys(
361+
List<RouteBase> subRoutes, GlobalKey<NavigatorState> navigatorKey) {
362+
for (final RouteBase route in subRoutes) {
363+
assert(
364+
route.parentNavigatorKey == null ||
365+
route.parentNavigatorKey == navigatorKey,
366+
"sub-route's parent navigator key must either be null or has the same navigator key as parent's key");
367+
if (route is GoRoute && route.redirectOnly) {
368+
// This route does not produce a page, need to check its sub-routes
369+
// instead.
370+
_debugCheckSubRouteParentNavigatorKeys(route.routes, navigatorKey);
371+
}
372+
}
373+
}
374+
341375
/// Attempts to build the Widget representing this shell route.
342376
///
343377
/// Returns null if this shell route does not build a Widget, but instead uses
@@ -506,12 +540,11 @@ class ShellRoute extends ShellRouteBase {
506540
}) : assert(routes.isNotEmpty),
507541
navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>(),
508542
super._() {
509-
for (final RouteBase route in routes) {
510-
if (route is GoRoute) {
511-
assert(route.parentNavigatorKey == null ||
512-
route.parentNavigatorKey == navigatorKey);
513-
}
514-
}
543+
assert(() {
544+
ShellRouteBase._debugCheckSubRouteParentNavigatorKeys(
545+
routes, this.navigatorKey);
546+
return true;
547+
}());
515548
}
516549

517550
/// The widget builder for a shell route.
@@ -576,6 +609,13 @@ class ShellRoute extends ShellRouteBase {
576609
@override
577610
Iterable<GlobalKey<NavigatorState>> get _navigatorKeys =>
578611
<GlobalKey<NavigatorState>>[navigatorKey];
612+
613+
@override
614+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
615+
super.debugFillProperties(properties);
616+
properties.add(DiagnosticsProperty<GlobalKey<NavigatorState>>(
617+
'navigatorKey', navigatorKey));
618+
}
579619
}
580620

581621
/// A route that displays a UI shell with separate [Navigator]s for its
@@ -828,6 +868,13 @@ class StatefulShellRoute extends ShellRouteBase {
828868
}
829869
return true;
830870
}
871+
872+
@override
873+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
874+
super.debugFillProperties(properties);
875+
properties.add(DiagnosticsProperty<Iterable<GlobalKey<NavigatorState>>>(
876+
'navigatorKeys', _navigatorKeys));
877+
}
831878
}
832879

833880
/// Representation of a separate branch in a stateful navigation tree, used to
@@ -854,7 +901,13 @@ class StatefulShellBranch {
854901
this.initialLocation,
855902
this.restorationScopeId,
856903
this.observers,
857-
}) : navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>();
904+
}) : navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>() {
905+
assert(() {
906+
ShellRouteBase._debugCheckSubRouteParentNavigatorKeys(
907+
routes, this.navigatorKey);
908+
return true;
909+
}());
910+
}
858911

859912
/// The [GlobalKey] to be used by the [Navigator] built for this branch.
860913
///

packages/go_router/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 9.0.0
4+
version: 9.0.1
55
repository: https://github.com/flutter/packages/tree/main/packages/go_router
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
77

packages/go_router/test/go_route_test.dart

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,127 @@ void main() {
155155
expect(find.text('Screen D'), findsOneWidget);
156156
expect(find.text('Screen C'), findsOneWidget);
157157
});
158+
159+
test('ShellRoute parent navigator key throw if not match', () async {
160+
final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>();
161+
final GlobalKey<NavigatorState> key2 = GlobalKey<NavigatorState>();
162+
bool hasError = false;
163+
try {
164+
ShellRoute(
165+
navigatorKey: key1,
166+
builder: (_, __, Widget child) => child,
167+
routes: <RouteBase>[
168+
ShellRoute(
169+
parentNavigatorKey: key2,
170+
builder: (_, __, Widget child) => child,
171+
routes: <RouteBase>[
172+
GoRoute(
173+
path: '1',
174+
builder: (_, __) => const Text('/route/1'),
175+
),
176+
],
177+
),
178+
],
179+
);
180+
} on AssertionError catch (_) {
181+
hasError = true;
182+
}
183+
expect(hasError, isTrue);
184+
});
185+
186+
group('Redirect only GoRoute', () {
187+
testWidgets('can redirect to subroute', (WidgetTester tester) async {
188+
final GoRouter router = await createRouter(
189+
<RouteBase>[
190+
GoRoute(
191+
path: '/',
192+
builder: (_, __) => const Text('home'),
193+
routes: <RouteBase>[
194+
GoRoute(
195+
path: 'route',
196+
redirect: (_, __) => '/route/1',
197+
routes: <RouteBase>[
198+
GoRoute(
199+
path: '1',
200+
builder: (_, __) => const Text('/route/1'),
201+
),
202+
],
203+
),
204+
],
205+
),
206+
],
207+
tester,
208+
);
209+
expect(find.text('home'), findsOneWidget);
210+
211+
router.go('/route');
212+
await tester.pumpAndSettle();
213+
// Should redirect to /route/1 without error.
214+
expect(find.text('/route/1'), findsOneWidget);
215+
216+
router.pop();
217+
await tester.pumpAndSettle();
218+
// Should go back directly to home page.
219+
expect(find.text('home'), findsOneWidget);
220+
});
221+
222+
testWidgets('throw if redirect to itself.', (WidgetTester tester) async {
223+
final GoRouter router = await createRouter(
224+
<RouteBase>[
225+
GoRoute(
226+
path: '/',
227+
builder: (_, __) => const Text('home'),
228+
routes: <RouteBase>[
229+
GoRoute(
230+
path: 'route',
231+
redirect: (_, __) => '/route',
232+
routes: <RouteBase>[
233+
GoRoute(
234+
path: '1',
235+
builder: (_, __) => const Text('/route/1'),
236+
),
237+
],
238+
),
239+
],
240+
),
241+
],
242+
tester,
243+
);
244+
expect(find.text('home'), findsOneWidget);
245+
246+
router.go('/route');
247+
await tester.pumpAndSettle();
248+
// Should redirect to /route/1 without error.
249+
expect(tester.takeException(), isAssertionError);
250+
});
251+
252+
testWidgets('throw if sub route does not conform with parent navigator key',
253+
(WidgetTester tester) async {
254+
final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>();
255+
final GlobalKey<NavigatorState> key2 = GlobalKey<NavigatorState>();
256+
bool hasError = false;
257+
try {
258+
ShellRoute(
259+
navigatorKey: key1,
260+
builder: (_, __, Widget child) => child,
261+
routes: <RouteBase>[
262+
GoRoute(
263+
path: '/',
264+
redirect: (_, __) => '/route',
265+
routes: <RouteBase>[
266+
GoRoute(
267+
parentNavigatorKey: key2,
268+
path: 'route',
269+
builder: (_, __) => const Text('/route/1'),
270+
),
271+
],
272+
),
273+
],
274+
);
275+
} on AssertionError catch (_) {
276+
hasError = true;
277+
}
278+
expect(hasError, isTrue);
279+
});
280+
});
158281
}

0 commit comments

Comments
 (0)