[go_router] Fixes crashes when popping navigators manually (#2952)
* [go_router] Fixes crashes when popping navigators manually
* rename
* add additional test case
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 075e950..c1fa3db 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 6.0.1
+
+- Fixes crashes when popping navigators manually.
+- Fixes trailing slashes after pops.
+
## 6.0.0
- **BREAKING CHANGE**
diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart
index 04c6656..238dd35 100644
--- a/packages/go_router/lib/src/builder.dart
+++ b/packages/go_router/lib/src/builder.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'configuration.dart';
@@ -50,6 +51,18 @@
final GoRouterStateRegistry _registry = GoRouterStateRegistry();
+ final Map<Page<Object?>, RouteMatch> _routeMatchLookUp =
+ <Page<Object?>, RouteMatch>{};
+
+ /// Looks the the [RouteMatch] for a given [Page].
+ ///
+ /// The [Page] must be in the latest [Navigator.pages]; otherwise, this method
+ /// returns null.
+ RouteMatch? getRouteMatchForPage(Page<Object?> page) =>
+ _routeMatchLookUp[page];
+
+ // final Map<>
+
/// Builds the top-level Navigator for the given [RouteMatchList].
Widget build(
BuildContext context,
@@ -57,6 +70,7 @@
PopPageCallback onPopPage,
bool routerNeglect,
) {
+ _routeMatchLookUp.clear();
if (matchList.isEmpty) {
// The build method can be called before async redirect finishes. Build a
// empty box until then.
@@ -119,10 +133,15 @@
GlobalKey<NavigatorState> navigatorKey,
Map<Page<Object?>, GoRouterState> registry) {
try {
+ assert(_routeMatchLookUp.isEmpty);
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
<GlobalKey<NavigatorState>, List<Page<Object?>>>{};
_buildRecursive(context, matchList, 0, onPopPage, routerNeglect,
keyToPage, navigatorKey, registry);
+
+ // Every Page should have a corresponding RouteMatch.
+ assert(keyToPage.values.flattened
+ .every((Page<Object?> page) => _routeMatchLookUp.containsKey(page)));
return keyToPage[navigatorKey]!;
} on _RouteBuilderError catch (e) {
return <Page<Object?>>[
@@ -271,12 +290,13 @@
page = null;
}
+ page ??= buildPage(context, state, Builder(builder: (BuildContext context) {
+ return _callRouteBuilder(context, state, match, childWidget: child);
+ }));
+ _routeMatchLookUp[page] = match;
+
// Return the result of the route's builder() or pageBuilder()
- return page ??
- // Uses a Builder to make sure its rebuild scope is limited to the page.
- buildPage(context, state, Builder(builder: (BuildContext context) {
- return _callRouteBuilder(context, state, match, childWidget: child);
- }));
+ return page;
}
/// Calls the user-provided route builder from the [RouteMatch]'s [RouteBase].
@@ -363,7 +383,7 @@
_cacheAppType(context);
return _pageBuilderForAppType!(
key: state.pageKey,
- name: state.name ?? state.fullpath,
+ name: state.name ?? state.path,
arguments: <String, String>{...state.params, ...state.queryParams},
restorationId: state.pageKey.value,
child: child,
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index b34aac0..07bcdf0 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -16,7 +16,7 @@
/// GoRouter implementation of [RouterDelegate].
class GoRouterDelegate extends RouterDelegate<RouteMatchList>
- with PopNavigatorRouterDelegateMixin<RouteMatchList>, ChangeNotifier {
+ with ChangeNotifier {
/// Constructor for GoRouter's implementation of the RouterDelegate base
/// class.
GoRouterDelegate({
@@ -132,7 +132,12 @@
if (!route.didPop(result)) {
return false;
}
- _matchList.pop();
+ final Page<Object?> page = route.settings as Page<Object?>;
+ final RouteMatch? match = builder.getRouteMatchForPage(page);
+ if (match == null) {
+ return true;
+ }
+ _matchList.remove(match);
notifyListeners();
assert(() {
_debugAssertMatchListNotEmpty();
@@ -146,7 +151,7 @@
/// See also:
/// * [push] which pushes the given location onto the page stack.
void pushReplacement(RouteMatchList matches) {
- _matchList.pop();
+ _matchList.remove(_matchList.last);
push(matches); // [push] will notify the listeners.
}
@@ -155,7 +160,6 @@
RouteMatchList get matches => _matchList;
/// For use by the Router architecture as part of the RouterDelegate.
- @override
GlobalKey<NavigatorState> get navigatorKey => _configuration.navigatorKey;
/// For use by the Router architecture as part of the RouterDelegate.
diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart
index 9d01fa5..62a4d55 100644
--- a/packages/go_router/lib/src/match.dart
+++ b/packages/go_router/lib/src/match.dart
@@ -73,6 +73,6 @@
/// An exception if there was an error during matching.
final Exception? error;
- /// Optional value key of type string, to hold a unique reference to a page.
+ /// Value key of type string, to hold a unique reference to a page.
final ValueKey<String> pageKey;
}
diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart
index 4297249..140c1d1 100644
--- a/packages/go_router/lib/src/matching.dart
+++ b/packages/go_router/lib/src/matching.dart
@@ -5,6 +5,7 @@
import 'package:flutter/widgets.dart';
import 'configuration.dart';
+import 'delegate.dart';
import 'match.dart';
import 'path_utils.dart';
@@ -56,7 +57,7 @@
static RouteMatchList empty =
RouteMatchList(<RouteMatch>[], Uri.parse(''), const <String, String>{});
- static String _generateFullPath(List<RouteMatch> matches) {
+ static String _generateFullPath(Iterable<RouteMatch> matches) {
final StringBuffer buffer = StringBuffer();
bool addsSlash = false;
for (final RouteMatch match in matches) {
@@ -96,17 +97,27 @@
_matches.add(match);
}
- /// Removes the last match.
- void pop() {
- if (_matches.last.route is GoRoute) {
- final GoRoute route = _matches.last.route as GoRoute;
- _uri = _uri.replace(path: removePatternFromPath(route.path, _uri.path));
- }
- _matches.removeLast();
+ /// Removes the match from the list.
+ void remove(RouteMatch match) {
+ final int index = _matches.indexOf(match);
+ assert(index != -1);
+ _matches.removeRange(index, _matches.length);
+
// Also pop ShellRoutes when there are no subsequent route matches
while (_matches.isNotEmpty && _matches.last.route is ShellRoute) {
_matches.removeLast();
}
+
+ final String fullPath = _generateFullPath(
+ _matches.where((RouteMatch match) => match is! ImperativeRouteMatch));
+ // Need to remove path parameters that are no longer in the fullPath.
+ final List<String> newParameters = <String>[];
+ patternToRegExp(fullPath, newParameters);
+ final Set<String> validParameters = newParameters.toSet();
+ pathParameters.removeWhere(
+ (String key, String value) => !validParameters.contains(key));
+
+ _uri = _uri.replace(path: patternToPath(fullPath, pathParameters));
}
/// An optional object provided by the app during navigation.
diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart
index d7a20ff..e9db923 100644
--- a/packages/go_router/lib/src/path_utils.dart
+++ b/packages/go_router/lib/src/path_utils.dart
@@ -47,44 +47,6 @@
return RegExp(buffer.toString(), caseSensitive: false);
}
-/// Removes string from the end of the path that matches a `pattern`.
-///
-/// The path parameters can be specified by prefixing them with `:`. The
-/// `parameters` are used for storing path parameter names.
-///
-///
-/// For example:
-///
-/// `path` = `/user/123/book/345`
-/// `pattern` = `book/:id`
-///
-/// The return value = `/user/123`.
-String removePatternFromPath(String pattern, String path) {
- final StringBuffer buffer = StringBuffer();
- int start = 0;
- for (final RegExpMatch match in _parameterRegExp.allMatches(pattern)) {
- if (match.start > start) {
- buffer.write(RegExp.escape(pattern.substring(start, match.start)));
- }
- final String? optionalPattern = match[2];
- final String regex =
- optionalPattern != null ? _escapeGroup(optionalPattern) : '[^/]+';
- buffer.write(regex);
- start = match.end;
- }
-
- if (start < pattern.length) {
- buffer.write(RegExp.escape(pattern.substring(start)));
- }
-
- if (!pattern.endsWith('/')) {
- buffer.write(r'(?=/|$)');
- }
- buffer.write(r'$');
- final RegExp regexp = RegExp(buffer.toString(), caseSensitive: false);
- return path.replaceFirst(regexp, '');
-}
-
String _escapeGroup(String group, [String? name]) {
final String escapedGroup = group.replaceFirstMapped(
RegExp(r'[:=!]'), (Match match) => '\\${match[0]}');
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index edcfd0a..46982a9 100644
--- a/packages/go_router/pubspec.yaml
+++ b/packages/go_router/pubspec.yaml
@@ -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: 6.0.0
+version: 6.0.1
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
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index 560a429..969b329 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -953,6 +953,41 @@
]);
});
+ testWidgets('on pop twice', (WidgetTester tester) async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const DummyScreen(),
+ routes: <RouteBase>[
+ GoRoute(
+ path: 'settings',
+ builder: (_, __) => const DummyScreen(),
+ routes: <RouteBase>[
+ GoRoute(
+ path: 'profile',
+ builder: (_, __) => const DummyScreen(),
+ ),
+ ]),
+ ]),
+ ];
+
+ final GoRouter router = await createRouter(routes, tester,
+ initialLocation: '/settings/profile');
+
+ log.clear();
+ router.pop();
+ router.pop();
+ await tester.pumpAndSettle();
+ expect(log, <Object>[
+ isMethodCall('selectMultiEntryHistory', arguments: null),
+ isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
+ 'location': '/',
+ 'state': null,
+ 'replace': false
+ }),
+ ]);
+ });
+
testWidgets('on pop with path parameters', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
@@ -1012,6 +1047,80 @@
]);
});
+ testWidgets('Can manually pop root navigator and display correct url',
+ (WidgetTester tester) async {
+ final GlobalKey<NavigatorState> rootNavigatorKey =
+ GlobalKey<NavigatorState>();
+
+ final List<RouteBase> routes = <RouteBase>[
+ GoRoute(
+ path: '/',
+ builder: (BuildContext context, GoRouterState state) {
+ return const Scaffold(
+ body: Text('Home'),
+ );
+ },
+ routes: <RouteBase>[
+ ShellRoute(
+ builder:
+ (BuildContext context, GoRouterState state, Widget child) {
+ return Scaffold(
+ appBar: AppBar(),
+ body: child,
+ );
+ },
+ routes: <RouteBase>[
+ GoRoute(
+ path: 'b',
+ builder: (BuildContext context, GoRouterState state) {
+ return const Scaffold(
+ body: Text('Screen B'),
+ );
+ },
+ 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 C'), findsOneWidget);
+ expect(log, <Object>[
+ isMethodCall('selectMultiEntryHistory', arguments: null),
+ isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
+ 'location': '/b/c',
+ 'state': null,
+ 'replace': false
+ }),
+ ]);
+
+ log.clear();
+ rootNavigatorKey.currentState!.pop();
+ await tester.pumpAndSettle();
+
+ expect(find.text('Home'), findsOneWidget);
+ expect(log, <Object>[
+ isMethodCall('selectMultiEntryHistory', arguments: null),
+ isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
+ 'location': '/',
+ 'state': null,
+ 'replace': false
+ }),
+ ]);
+ });
+
testWidgets('works correctly with async redirect',
(WidgetTester tester) async {
final UniqueKey login = UniqueKey();