diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index dcaac3c3001..a2fc66f5599 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 56bd9579d67..a4fa36aa04e 100644 --- a/packages/go_router/lib/go_router.dart +++ b/packages/go_router/lib/go_router.dart @@ -70,6 +70,35 @@ extension GoRouterHelper on BuildContext { 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 params = const {}, + Map queryParams = const {}, + 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 17a7af14145..902c10f2e7c 100644 --- a/packages/go_router/lib/src/go_router.dart +++ b/packages/go_router/lib/src/go_router.dart @@ -160,6 +160,41 @@ class GoRouter extends ChangeNotifier with NavigatorObserver { 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((List 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 params = const {}, + Map queryParams = const {}, + 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 3ea7e624ea7..25830de7758 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 @@ class GoRouterDelegate extends RouterDelegate> 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 422a54c3253..ec58f361a91 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 73592a27db5..2716e1852eb 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 @@ void main() { ); }); + group('replace', () { + testWidgets( + 'It should replace the last match with the given one', + (WidgetTester tester) async { + final GoRouter goRouter = GoRouter( + initialLocation: '/', + routes: [ + 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(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() + .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();