Skip to content

Commit 010ba50

Browse files
authored
[go_router] Refactors imperative APIs and browser history (#4134)
Several thing. 1. I move all the imperative logic from RouterDelegate to RouteInformationParser, so that the imperative API can go through Router parsing pipeline. The Parser will handle modifying mutating RouteMatchList and produce the final RouteMatchList. The RouterDelegate would only focus on building the widget base on the final RouteMatchList 2. combine RouteMatcher and Redirector with RouteConfiguration. I feel that instead of passing three class instances around, we should probably just have one class for all the route parsing related utility. 3. serialize routeMatchList and store into browser history. This way we can let backward and forward button to reflect imperative operation as well. 4. Some minor clean ups
1 parent e37dd83 commit 010ba50

24 files changed

+1622
-1476
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
## 8.0.0
2+
3+
- **BREAKING CHANGE**:
4+
- Imperatively pushed GoRoute no longer change URL.
5+
- Browser backward and forward button respects imperative route operations.
6+
- Refactors the route parsing pipeline.
7+
18
## 7.1.1
29

3-
* Removes obsolete null checks on non-nullable values.
10+
- Removes obsolete null checks on non-nullable values.
411

512
## 7.1.0
613

packages/go_router/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ See the API documentation for details on the following topics:
3737
- [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html)
3838

3939
## Migration guides
40-
- [Migrating to 7.0.0](https://docs.google.com/document/d/10Xbpifbs4E-zh6YE5akIO8raJq_m3FIXs6nUGdOspOg).
40+
- [Migrating to 8.0.0](https://flutter.dev/go/go-router-v8-breaking-changes).
41+
- [Migrating to 7.0.0](https://flutter.dev/go/go-router-v7-breaking-changes).
4142
- [Migrating to 6.0.0](https://flutter.dev/go/go-router-v6-breaking-changes)
4243
- [Migrating to 5.1.2](https://flutter.dev/go/go-router-v5-1-2-breaking-changes)
4344
- [Migrating to 5.0](https://flutter.dev/go/go-router-v5-breaking-changes)

packages/go_router/lib/src/builder.dart

Lines changed: 60 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import '../go_router.dart';
99
import 'configuration.dart';
1010
import 'logging.dart';
1111
import 'match.dart';
12-
import 'matching.dart';
1312
import 'misc/error_screen.dart';
13+
import 'misc/errors.dart';
1414
import 'pages/cupertino.dart';
1515
import 'pages/material.dart';
1616
import 'route_data.dart';
@@ -83,7 +83,7 @@ class RouteBuilder {
8383
RouteMatchList matchList,
8484
bool routerNeglect,
8585
) {
86-
if (matchList.isEmpty) {
86+
if (matchList.isEmpty && !matchList.isError) {
8787
// The build method can be called before async redirect finishes. Build a
8888
// empty box until then.
8989
return const SizedBox.shrink();
@@ -92,18 +92,12 @@ class RouteBuilder {
9292
context,
9393
Builder(
9494
builder: (BuildContext context) {
95-
try {
96-
final Map<Page<Object?>, GoRouterState> newRegistry =
97-
<Page<Object?>, GoRouterState>{};
98-
final Widget result = tryBuild(context, matchList, routerNeglect,
99-
configuration.navigatorKey, newRegistry);
100-
_registry.updateRegistry(newRegistry);
101-
return GoRouterStateRegistryScope(
102-
registry: _registry, child: result);
103-
} on _RouteBuilderError catch (e) {
104-
return _buildErrorNavigator(context, e, matchList.uri,
105-
onPopPageWithRouteMatch, configuration.navigatorKey);
106-
}
95+
final Map<Page<Object?>, GoRouterState> newRegistry =
96+
<Page<Object?>, GoRouterState>{};
97+
final Widget result = tryBuild(context, matchList, routerNeglect,
98+
configuration.navigatorKey, newRegistry);
99+
_registry.updateRegistry(newRegistry);
100+
return GoRouterStateRegistryScope(registry: _registry, child: result);
107101
},
108102
),
109103
);
@@ -147,28 +141,31 @@ class RouteBuilder {
147141
bool routerNeglect,
148142
GlobalKey<NavigatorState> navigatorKey,
149143
Map<Page<Object?>, GoRouterState> registry) {
150-
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
151-
<GlobalKey<NavigatorState>, List<Page<Object?>>>{};
152-
try {
144+
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage;
145+
if (matchList.isError) {
146+
keyToPage = <GlobalKey<NavigatorState>, List<Page<Object?>>>{
147+
navigatorKey: <Page<Object?>>[
148+
_buildErrorPage(
149+
context, _buildErrorState(matchList.error!, matchList.uri)),
150+
]
151+
};
152+
} else {
153+
keyToPage = <GlobalKey<NavigatorState>, List<Page<Object?>>>{};
153154
_buildRecursive(context, matchList, 0, pagePopContext, routerNeglect,
154155
keyToPage, navigatorKey, registry);
155156

156157
// Every Page should have a corresponding RouteMatch.
157158
assert(keyToPage.values.flattened.every((Page<Object?> page) =>
158159
pagePopContext.getRouteMatchForPage(page) != null));
159-
return keyToPage[navigatorKey]!;
160-
} on _RouteBuilderError catch (e) {
161-
return <Page<Object?>>[
162-
_buildErrorPage(context, e, matchList.uri),
163-
];
164-
} finally {
165-
/// Clean up previous cache to prevent memory leak, making sure any nested
166-
/// stateful shell routes for the current match list are kept.
167-
final Set<Key> activeKeys = keyToPage.keys.toSet()
168-
..addAll(_nestedStatefulNavigatorKeys(matchList));
169-
_goHeroCache.removeWhere(
170-
(GlobalKey<NavigatorState> key, _) => !activeKeys.contains(key));
171160
}
161+
162+
/// Clean up previous cache to prevent memory leak, making sure any nested
163+
/// stateful shell routes for the current match list are kept.
164+
final Set<Key> activeKeys = keyToPage.keys.toSet()
165+
..addAll(_nestedStatefulNavigatorKeys(matchList));
166+
_goHeroCache.removeWhere(
167+
(GlobalKey<NavigatorState> key, _) => !activeKeys.contains(key));
168+
return keyToPage[navigatorKey]!;
172169
}
173170

174171
static Set<GlobalKey<NavigatorState>> _nestedStatefulNavigatorKeys(
@@ -200,15 +197,15 @@ class RouteBuilder {
200197
}
201198
final RouteMatch match = matchList.matches[startIndex];
202199

203-
if (match.error != null) {
204-
throw _RouteBuilderError('Match error found during build phase',
205-
exception: match.error);
206-
}
207-
208200
final RouteBase route = match.route;
209201
final GoRouterState state = buildState(matchList, match);
210202
Page<Object?>? page;
211-
if (route is GoRoute) {
203+
if (state.error != null) {
204+
page = _buildErrorPage(context, state);
205+
keyToPages.putIfAbsent(navigatorKey, () => <Page<Object?>>[]).add(page);
206+
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
207+
routerNeglect, keyToPages, navigatorKey, registry);
208+
} else if (route is GoRoute) {
212209
page = _buildPageForGoRoute(context, state, match, route, pagePopContext);
213210
// If this GoRoute is for a different Navigator, add it to the
214211
// list of out of scope pages
@@ -284,7 +281,7 @@ class RouteBuilder {
284281
registry[page] = state;
285282
pagePopContext._setRouteMatchForPage(page, match);
286283
} else {
287-
throw _RouteBuilderException('Unsupported route type $route');
284+
throw GoError('Unsupported route type $route');
288285
}
289286
}
290287

@@ -324,8 +321,17 @@ class RouteBuilder {
324321
name = route.name;
325322
path = route.path;
326323
}
327-
final RouteMatchList effectiveMatchList =
328-
match is ImperativeRouteMatch ? match.matches : matchList;
324+
final RouteMatchList effectiveMatchList;
325+
if (match is ImperativeRouteMatch) {
326+
effectiveMatchList = match.matches;
327+
if (effectiveMatchList.isError) {
328+
return _buildErrorState(
329+
effectiveMatchList.error!, effectiveMatchList.uri);
330+
}
331+
} else {
332+
effectiveMatchList = matchList;
333+
assert(!effectiveMatchList.isError);
334+
}
329335
return GoRouterState(
330336
configuration,
331337
location: effectiveMatchList.uri.toString(),
@@ -335,10 +341,10 @@ class RouteBuilder {
335341
fullPath: effectiveMatchList.fullPath,
336342
pathParameters:
337343
Map<String, String>.from(effectiveMatchList.pathParameters),
338-
error: match.error,
344+
error: effectiveMatchList.error,
339345
queryParameters: effectiveMatchList.uri.queryParameters,
340346
queryParametersAll: effectiveMatchList.uri.queryParametersAll,
341-
extra: match.extra,
347+
extra: effectiveMatchList.extra,
342348
pageKey: match.pageKey,
343349
);
344350
}
@@ -370,7 +376,7 @@ class RouteBuilder {
370376
final GoRouterWidgetBuilder? builder = route.builder;
371377

372378
if (builder == null) {
373-
throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route');
379+
throw GoError('No routeBuilder provided to GoRoute: $route');
374380
}
375381

376382
return builder(context, state);
@@ -405,7 +411,7 @@ class RouteBuilder {
405411
final Widget? widget =
406412
route.buildWidget(context, state, shellRouteContext!);
407413
if (widget == null) {
408-
throw _RouteBuilderError('No builder provided to ShellRoute: $route');
414+
throw GoError('No builder provided to ShellRoute: $route');
409415
}
410416

411417
return widget;
@@ -485,38 +491,26 @@ class RouteBuilder {
485491
child: child,
486492
);
487493

488-
/// Builds a Navigator containing an error page.
489-
Widget _buildErrorNavigator(
490-
BuildContext context,
491-
_RouteBuilderError e,
492-
Uri uri,
493-
PopPageWithRouteMatchCallback onPopPage,
494-
GlobalKey<NavigatorState> navigatorKey) {
495-
return _buildNavigator(
496-
(Route<dynamic> route, dynamic result) => onPopPage(route, result, null),
497-
<Page<Object?>>[
498-
_buildErrorPage(context, e, uri),
499-
],
500-
navigatorKey,
501-
);
502-
}
503-
504-
/// Builds a an error page.
505-
Page<void> _buildErrorPage(
506-
BuildContext context,
507-
_RouteBuilderError error,
494+
GoRouterState _buildErrorState(
495+
Exception error,
508496
Uri uri,
509497
) {
510-
final GoRouterState state = GoRouterState(
498+
final String location = uri.toString();
499+
return GoRouterState(
511500
configuration,
512-
location: uri.toString(),
501+
location: location,
513502
matchedLocation: uri.path,
514503
name: null,
515504
queryParameters: uri.queryParameters,
516505
queryParametersAll: uri.queryParametersAll,
517-
error: Exception(error),
518-
pageKey: const ValueKey<String>('error'),
506+
error: error,
507+
pageKey: ValueKey<String>('$location(error)'),
519508
);
509+
}
510+
511+
/// Builds a an error page.
512+
Page<void> _buildErrorPage(BuildContext context, GoRouterState state) {
513+
assert(state.error != null);
520514

521515
// If the error page builder is provided, use that, otherwise, if the error
522516
// builder is provided, wrap that in an app-specific page (for example,
@@ -556,43 +550,6 @@ typedef _PageBuilderForAppType = Page<void> Function({
556550
required Widget child,
557551
});
558552

559-
/// An error that occurred while building the app's UI based on the route
560-
/// matches.
561-
class _RouteBuilderError extends Error {
562-
/// Constructs a [_RouteBuilderError].
563-
_RouteBuilderError(this.message, {this.exception});
564-
565-
/// The error message.
566-
final String message;
567-
568-
/// The exception that occurred.
569-
final Exception? exception;
570-
571-
@override
572-
String toString() {
573-
return '$message ${exception ?? ""}';
574-
}
575-
}
576-
577-
/// An error that occurred while building the app's UI based on the route
578-
/// matches.
579-
class _RouteBuilderException implements Exception {
580-
/// Constructs a [_RouteBuilderException].
581-
//ignore: unused_element
582-
_RouteBuilderException(this.message, {this.exception});
583-
584-
/// The error message.
585-
final String message;
586-
587-
/// The exception that occurred.
588-
final Exception? exception;
589-
590-
@override
591-
String toString() {
592-
return '$message ${exception ?? ""}';
593-
}
594-
}
595-
596553
/// Context used to provide a route to page association when popping routes.
597554
class _PagePopContext {
598555
_PagePopContext._(this.onPopPageWithRouteMatch);

0 commit comments

Comments
 (0)