[go_router] Adds `void replace()` and `replaceNamed` to `GoRouterDelegate`, `GoRouter` and `GoRouterHelper` (#2306)
* :sparkles: Add replace and replaceNamed
* :white_check_mark: Write a test for replace
* :arrow_up: :memo: Update version number and changelog
* :memo: Improve the documentation
* :art: Add missing extra line in changelog
* :white_check_mark: Add a test for replaceNamed
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index dcaac3c..a2fc66f 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 4.2.0
+
+- Adds `void replace()` and `replaceNamed` to `GoRouterDelegate`, `GoRouter` and `GoRouterHelper`.
+
## 4.1.1
- Fixes a bug where calling namedLocation does not support case-insensitive way.
diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart
index 56bd957..a4fa36a 100644
--- a/packages/go_router/lib/go_router.dart
+++ b/packages/go_router/lib/go_router.dart
@@ -70,6 +70,35 @@
extra: extra,
);
+ /// Replaces the top-most page of the page stack with the given URL location
+ /// w/ optional query parameters, e.g. `/family/f2/person/p1?color=blue`.
+ ///
+ /// See also:
+ /// * [go] which navigates to the location.
+ /// * [push] which pushes the location onto the page stack.
+ void replace(String location, {Object? extra}) =>
+ GoRouter.of(this).replace(location, extra: extra);
+
+ /// Replaces the top-most page of the page stack with the named route w/
+ /// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid':
+ /// 'p1'}`.
+ ///
+ /// See also:
+ /// * [goNamed] which navigates a named route.
+ /// * [pushNamed] which pushes a named route onto the page stack.
+ void replaceNamed(
+ String name, {
+ Map<String, String> params = const <String, String>{},
+ Map<String, String> queryParams = const <String, String>{},
+ Object? extra,
+ }) =>
+ GoRouter.of(this).replaceNamed(
+ name,
+ params: params,
+ queryParams: queryParams,
+ extra: extra,
+ );
+
/// Returns `true` if there is more than 1 page on the stack.
bool canPop() => GoRouter.of(this).canPop();
diff --git a/packages/go_router/lib/src/go_router.dart b/packages/go_router/lib/src/go_router.dart
index 17a7af1..902c10f 100644
--- a/packages/go_router/lib/src/go_router.dart
+++ b/packages/go_router/lib/src/go_router.dart
@@ -160,6 +160,41 @@
extra: extra,
);
+ /// Replaces the top-most page of the page stack with the given URL location
+ /// w/ optional query parameters, e.g. `/family/f2/person/p1?color=blue`.
+ ///
+ /// See also:
+ /// * [go] which navigates to the location.
+ /// * [push] which pushes the location onto the page stack.
+ void replace(String location, {Object? extra}) {
+ routeInformationParser
+ .parseRouteInformation(
+ DebugGoRouteInformation(location: location, state: extra),
+ )
+ .then<void>((List<GoRouteMatch> matches) {
+ routerDelegate.replace(matches.last);
+ });
+ }
+
+ /// Replaces the top-most page of the page stack with the named route w/
+ /// optional parameters, e.g. `name='person', params={'fid': 'f2', 'pid':
+ /// 'p1'}`.
+ ///
+ /// See also:
+ /// * [goNamed] which navigates a named route.
+ /// * [pushNamed] which pushes a named route onto the page stack.
+ void replaceNamed(
+ String name, {
+ Map<String, String> params = const <String, String>{},
+ Map<String, String> queryParams = const <String, String>{},
+ Object? extra,
+ }) {
+ replace(
+ namedLocation(name, params: params, queryParams: queryParams),
+ extra: extra,
+ );
+ }
+
/// Returns `true` if there is more than 1 page on the stack.
bool canPop() => routerDelegate.canPop();
diff --git a/packages/go_router/lib/src/go_router_delegate.dart b/packages/go_router/lib/src/go_router_delegate.dart
index 3ea7e62..25830de 100644
--- a/packages/go_router/lib/src/go_router_delegate.dart
+++ b/packages/go_router/lib/src/go_router_delegate.dart
@@ -65,6 +65,15 @@
notifyListeners();
}
+ /// Replaces the top-most page of the page stack with the given one.
+ ///
+ /// See also:
+ /// * [push] which pushes the given location onto the page stack.
+ void replace(GoRouteMatch match) {
+ _matches.last = match;
+ notifyListeners();
+ }
+
/// Returns `true` if there is more than 1 page on the stack.
bool canPop() {
return _matches.length > 1;
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index 422a54c..ec58f36 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: 4.1.1
+version: 4.2.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
diff --git a/packages/go_router/test/go_router_delegate_test.dart b/packages/go_router/test/go_router_delegate_test.dart
index 73592a2..2716e18 100644
--- a/packages/go_router/test/go_router_delegate_test.dart
+++ b/packages/go_router/test/go_router_delegate_test.dart
@@ -78,6 +78,114 @@
);
});
+ group('replace', () {
+ testWidgets(
+ 'It should replace the last match with the given one',
+ (WidgetTester tester) async {
+ final GoRouter goRouter = GoRouter(
+ initialLocation: '/',
+ routes: <GoRoute>[
+ GoRoute(path: '/', builder: (_, __) => const SizedBox()),
+ GoRoute(path: '/page-0', builder: (_, __) => const SizedBox()),
+ GoRoute(path: '/page-1', builder: (_, __) => const SizedBox()),
+ ],
+ );
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routeInformationProvider: goRouter.routeInformationProvider,
+ routeInformationParser: goRouter.routeInformationParser,
+ routerDelegate: goRouter.routerDelegate,
+ ),
+ );
+
+ goRouter.push('/page-0');
+
+ goRouter.routerDelegate.addListener(expectAsync0(() {}));
+ final GoRouteMatch first = goRouter.routerDelegate.matches.first;
+ final GoRouteMatch last = goRouter.routerDelegate.matches.last;
+ goRouter.replace('/page-1');
+ expect(goRouter.routerDelegate.matches.length, 2);
+ expect(
+ goRouter.routerDelegate.matches.first,
+ first,
+ reason: 'The first match should still be in the list of matches',
+ );
+ expect(
+ goRouter.routerDelegate.matches.last,
+ isNot(last),
+ reason: 'The last match should have been removed',
+ );
+ expect(
+ goRouter.routerDelegate.matches.last.fullpath,
+ '/page-1',
+ reason: 'The new location should have been pushed',
+ );
+ },
+ );
+ });
+
+ group('replaceNamed', () {
+ testWidgets(
+ 'It should replace the last match with the given one',
+ (WidgetTester tester) async {
+ final GoRouter goRouter = GoRouter(
+ initialLocation: '/',
+ routes: <GoRoute>[
+ GoRoute(path: '/', builder: (_, __) => const SizedBox()),
+ GoRoute(
+ path: '/page-0',
+ name: 'page0',
+ builder: (_, __) => const SizedBox()),
+ GoRoute(
+ path: '/page-1',
+ name: 'page1',
+ builder: (_, __) => const SizedBox()),
+ ],
+ );
+ await tester.pumpWidget(
+ MaterialApp.router(
+ routeInformationProvider: goRouter.routeInformationProvider,
+ routeInformationParser: goRouter.routeInformationParser,
+ routerDelegate: goRouter.routerDelegate,
+ ),
+ );
+
+ goRouter.pushNamed('page0');
+
+ goRouter.routerDelegate.addListener(expectAsync0(() {}));
+ final GoRouteMatch first = goRouter.routerDelegate.matches.first;
+ final GoRouteMatch last = goRouter.routerDelegate.matches.last;
+ goRouter.replaceNamed('page1');
+ expect(goRouter.routerDelegate.matches.length, 2);
+ expect(
+ goRouter.routerDelegate.matches.first,
+ first,
+ reason: 'The first match should still be in the list of matches',
+ );
+ expect(
+ goRouter.routerDelegate.matches.last,
+ isNot(last),
+ reason: 'The last match should have been removed',
+ );
+ expect(
+ goRouter.routerDelegate.matches.last,
+ isA<GoRouteMatch>()
+ .having(
+ (GoRouteMatch match) => match.fullpath,
+ 'match.fullpath',
+ '/page-1',
+ )
+ .having(
+ (GoRouteMatch match) => match.route.name,
+ 'match.route.name',
+ 'page1',
+ ),
+ reason: 'The new location should have been pushed',
+ );
+ },
+ );
+ });
+
testWidgets('dispose unsubscribes from refreshListenable',
(WidgetTester tester) async {
final FakeRefreshListenable refreshListenable = FakeRefreshListenable();