[go_router] fixes pop and push to update urls correctly (#2904)
* [go_router] fixes pop and push to update urls correctly
* bump version
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index ff7402e..56459e2 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 5.2.2
+
+- Fixes `pop` and `push` to update urls correctly.
+
## 5.2.1
- Refactors `GoRouter.pop` to be able to pop individual pageless route with result.
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 3bb303c..82a6e6d 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -133,6 +133,7 @@
return false;
}
_matchList.pop();
+ notifyListeners();
assert(() {
_debugAssertMatchListNotEmpty();
return true;
diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart
index d9027f4..4297249 100644
--- a/packages/go_router/lib/src/matching.dart
+++ b/packages/go_router/lib/src/matching.dart
@@ -48,7 +48,7 @@
/// The list of [RouteMatch] objects.
class RouteMatchList {
/// RouteMatchList constructor.
- RouteMatchList(List<RouteMatch> matches, this.uri, this.pathParameters)
+ RouteMatchList(List<RouteMatch> matches, this._uri, this.pathParameters)
: _matches = matches,
fullpath = _generateFullPath(matches);
@@ -82,7 +82,8 @@
final Map<String, String> pathParameters;
/// The uri of the current match.
- final Uri uri;
+ Uri get uri => _uri;
+ Uri _uri;
/// Returns true if there are no matches.
bool get isEmpty => _matches.isEmpty;
@@ -97,8 +98,11 @@
/// 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();
-
// Also pop ShellRoutes when there are no subsequent route matches
while (_matches.isNotEmpty && _matches.last.route is ShellRoute) {
_matches.removeLast();
diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart
index e15fba3..b83923d 100644
--- a/packages/go_router/lib/src/parser.dart
+++ b/packages/go_router/lib/src/parser.dart
@@ -8,6 +8,7 @@
import 'package:flutter/widgets.dart';
import 'configuration.dart';
+import 'delegate.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'matching.dart';
@@ -98,6 +99,10 @@
/// for use by the Router architecture as part of the RouteInformationParser
@override
RouteInformation restoreRouteInformation(RouteMatchList configuration) {
+ if (configuration.matches.last is ImperativeRouteMatch) {
+ configuration =
+ (configuration.matches.last as ImperativeRouteMatch).matches;
+ }
return RouteInformation(
location: configuration.uri.toString(),
state: configuration.extra,
diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart
index 804e0ee..d7a20ff 100644
--- a/packages/go_router/lib/src/path_utils.dart
+++ b/packages/go_router/lib/src/path_utils.dart
@@ -47,10 +47,51 @@
return RegExp(buffer.toString(), caseSensitive: false);
}
-String _escapeGroup(String group, String name) {
+/// 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]}');
- return '(?<$name>$escapedGroup)';
+ if (name != null) {
+ return '(?<$name>$escapedGroup)';
+ }
+ return escapedGroup;
}
/// Reconstructs the full path from a [pattern] and path parameters.
diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart
index 5e58f0c..4a06b54 100644
--- a/packages/go_router/lib/src/route.dart
+++ b/packages/go_router/lib/src/route.dart
@@ -300,6 +300,7 @@
/// 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) =>
_pathRE.matchAsPrefix(loc) as RegExpMatch?;
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index 64eb598..9fbfc28 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: 5.2.1
+version: 5.2.2
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 35198e0..023fe57 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -879,6 +879,138 @@
});
});
+ group('report correct url', () {
+ final List<MethodCall> log = <MethodCall>[];
+ setUp(() {
+ TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
+ .setMockMethodCallHandler(SystemChannels.navigation,
+ (MethodCall methodCall) async {
+ log.add(methodCall);
+ return null;
+ });
+ });
+ tearDown(() {
+ TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
+ .setMockMethodCallHandler(SystemChannels.navigation, null);
+ log.clear();
+ });
+
+ testWidgets('on push', (WidgetTester tester) async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const DummyScreen(),
+ ),
+ GoRoute(
+ path: '/settings',
+ builder: (_, __) => const DummyScreen(),
+ ),
+ ];
+
+ final GoRouter router = await createRouter(routes, tester);
+
+ log.clear();
+ router.push('/settings');
+ await tester.pumpAndSettle();
+ expect(log, <Object>[
+ isMethodCall('selectMultiEntryHistory', arguments: null),
+ isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
+ 'location': '/settings',
+ 'state': null,
+ 'replace': false
+ }),
+ ]);
+ });
+
+ testWidgets('on pop', (WidgetTester tester) async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const DummyScreen(),
+ routes: <RouteBase>[
+ GoRoute(
+ path: 'settings',
+ builder: (_, __) => const DummyScreen(),
+ ),
+ ]),
+ ];
+
+ final GoRouter router =
+ await createRouter(routes, tester, initialLocation: '/settings');
+
+ log.clear();
+ 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(
+ path: '/',
+ builder: (_, __) => const DummyScreen(),
+ routes: <RouteBase>[
+ GoRoute(
+ path: 'settings/:id',
+ builder: (_, __) => const DummyScreen(),
+ ),
+ ]),
+ ];
+
+ final GoRouter router =
+ await createRouter(routes, tester, initialLocation: '/settings/123');
+
+ log.clear();
+ 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 case 2',
+ (WidgetTester tester) async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const DummyScreen(),
+ routes: <RouteBase>[
+ GoRoute(
+ path: ':id',
+ builder: (_, __) => const DummyScreen(),
+ ),
+ ]),
+ ];
+
+ final GoRouter router =
+ await createRouter(routes, tester, initialLocation: '/123/');
+
+ log.clear();
+ router.pop();
+ await tester.pumpAndSettle();
+ expect(log, <Object>[
+ isMethodCall('selectMultiEntryHistory', arguments: null),
+ isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
+ 'location': '/',
+ 'state': null,
+ 'replace': false
+ }),
+ ]);
+ });
+ });
+
group('named routes', () {
testWidgets('match home route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[