From d3a7cc28ada78e1c25b87b5031602a1c5d526696 Mon Sep 17 00:00:00 2001 From: "marcin.pawlowski" Date: Fri, 24 May 2024 09:45:22 +0200 Subject: [PATCH 1/2] [go_router]: add optional completer for replaced routes --- packages/go_router/CHANGELOG.md | 4 + .../lib/src/information_provider.dart | 32 +++++-- packages/go_router/lib/src/match.dart | 23 ++++- .../go_router/lib/src/misc/extensions.dart | 54 +++++++++--- packages/go_router/lib/src/parser.dart | 11 +++ packages/go_router/lib/src/router.dart | 28 +++++- packages/go_router/pubspec.yaml | 2 +- packages/go_router/test/delegate_test.dart | 88 +++++++++++++++++++ packages/go_router/test/inherited_test.dart | 13 ++- packages/go_router/test/test_helpers.dart | 8 +- 10 files changed, 233 insertions(+), 30 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 7b3e451ba90..1d65e2fb045 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 14.1.4 + +- Add optional completer for replaced routes ([#141251](https://github.com/flutter/flutter/issues/141251)) + ## 14.1.3 - Improves the logging of routes when `debugLogDiagnostics` is enabled or `debugKnownRoutes() is called. Explains the position of shell routes in the route tree. Prints the widget name of the routes it is building. diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index dc979193b32..f624953a0a9 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -46,6 +46,7 @@ class RouteInformationState { RouteInformationState({ this.extra, this.completer, + this.onReplaceCompleter, this.baseRouteMatchList, required this.type, }) : assert((type == NavigatingType.go || type == NavigatingType.restore) == @@ -62,6 +63,10 @@ class RouteInformationState { /// [NavigatingType.restore]. final Completer? completer; + /// The completer that needs to be completed when route is replaced. + /// [completer] is ignored when replacing routes. + final Completer? onReplaceCompleter; + /// The base route match list to push on top to. /// /// This is only null if [type] is [NavigatingType.go]. @@ -146,8 +151,12 @@ class GoRouteInformationProvider extends RouteInformationProvider } /// Pushes the `location` as a new route on top of `base`. - Future push(String location, - {required RouteMatchList base, Object? extra}) { + Future push( + String location, { + required RouteMatchList base, + Object? extra, + Completer? onReplaceCompleter, + }) { final Completer completer = Completer(); _setValue( location, @@ -156,6 +165,7 @@ class GoRouteInformationProvider extends RouteInformationProvider baseRouteMatchList: base, completer: completer, type: NavigatingType.push, + onReplaceCompleter: onReplaceCompleter, ), ); return completer.future; @@ -186,8 +196,12 @@ class GoRouteInformationProvider extends RouteInformationProvider /// Removes the top-most route match from `base` and pushes the `location` as a /// new route on top. - Future pushReplacement(String location, - {required RouteMatchList base, Object? extra}) { + Future pushReplacement( + String location, { + required RouteMatchList base, + Object? extra, + Completer? onReplaceCompleter, + }) { final Completer completer = Completer(); _setValue( location, @@ -196,14 +210,19 @@ class GoRouteInformationProvider extends RouteInformationProvider baseRouteMatchList: base, completer: completer, type: NavigatingType.pushReplacement, + onReplaceCompleter: onReplaceCompleter, ), ); return completer.future; } /// Replaces the top-most route match from `base` with the `location`. - Future replace(String location, - {required RouteMatchList base, Object? extra}) { + Future replace( + String location, { + required RouteMatchList base, + Object? extra, + Completer? onReplaceCompleter, + }) { final Completer completer = Completer(); _setValue( location, @@ -212,6 +231,7 @@ class GoRouteInformationProvider extends RouteInformationProvider baseRouteMatchList: base, completer: completer, type: NavigatingType.replace, + onReplaceCompleter: onReplaceCompleter, ), ); return completer.future; diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index fba08f83865..e0705db46df 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -421,9 +421,12 @@ class ShellRouteMatch extends RouteMatchBase { /// The route match that represent route pushed through [GoRouter.push]. class ImperativeRouteMatch extends RouteMatch { /// Constructor for [ImperativeRouteMatch]. - ImperativeRouteMatch( - {required super.pageKey, required this.matches, required this.completer}) - : super( + ImperativeRouteMatch({ + required super.pageKey, + required this.matches, + required this.completer, + this.onReplaceCompleter, + }) : super( route: _getsLastRouteFromMatches(matches), matchedLocation: _getsMatchedLocationFromMatches(matches), ); @@ -449,6 +452,12 @@ class ImperativeRouteMatch extends RouteMatch { /// The completer for the future returned by [GoRouter.push]. final Completer completer; + /// The completer for the future when [GoRouter.pushReplacement] + /// [GoRouter.replace] replaces the current route. + /// + /// Completer from the method call is put into the void when route is replaced. + final Completer? onReplaceCompleter; + /// Called when the corresponding [Route] associated with this route match is /// completed. void complete([dynamic value]) { @@ -466,11 +475,17 @@ class ImperativeRouteMatch extends RouteMatch { return other is ImperativeRouteMatch && completer == other.completer && matches == other.matches && + onReplaceCompleter == other.onReplaceCompleter && super == other; } @override - int get hashCode => Object.hash(super.hashCode, completer, matches.hashCode); + int get hashCode => Object.hash( + super.hashCode, + completer, + matches.hashCode, + onReplaceCompleter, + ); } /// The list of [RouteMatchBase] objects. diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index c137022b802..846b9bfb333 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:flutter/widgets.dart'; import '../router.dart'; @@ -46,8 +48,16 @@ extension GoRouterHelper on BuildContext { /// * [replace] which replaces the top-most page of the page stack but treats /// it as the same page. The page key will be reused. This will preserve the /// state and not run any page animation. - Future push(String location, {Object? extra}) => - GoRouter.of(this).push(location, extra: extra); + Future push( + String location, { + Object? extra, + Completer? onReplaceCompleter, + }) => + GoRouter.of(this).push( + location, + extra: extra, + onReplaceCompleter: onReplaceCompleter, + ); /// Navigate to a named route onto the page stack. Future pushNamed( @@ -55,12 +65,14 @@ extension GoRouterHelper on BuildContext { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + Completer? onReplaceCompleter, }) => GoRouter.of(this).pushNamed( name, pathParameters: pathParameters, queryParameters: queryParameters, extra: extra, + onReplaceCompleter: onReplaceCompleter, ); /// Returns `true` if there is more than 1 page on the stack. @@ -79,8 +91,16 @@ extension GoRouterHelper on BuildContext { /// * [replace] which replaces the top-most page of the page stack but treats /// it as the same page. The page key will be reused. This will preserve the /// state and not run any page animation. - void pushReplacement(String location, {Object? extra}) => - GoRouter.of(this).pushReplacement(location, extra: extra); + void pushReplacement( + String location, { + Object? extra, + Completer? onReplaceCompleter, + }) => + GoRouter.of(this).pushReplacement( + location, + extra: extra, + onReplaceCompleter: onReplaceCompleter, + ); /// Replaces the top-most page of the page stack with the named route w/ /// optional parameters, e.g. `name='person', pathParameters={'fid': 'f2', 'pid': @@ -94,12 +114,14 @@ extension GoRouterHelper on BuildContext { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + Completer? onReplaceCompleter, }) => GoRouter.of(this).pushReplacementNamed( name, pathParameters: pathParameters, queryParameters: queryParameters, extra: extra, + onReplaceCompleter: onReplaceCompleter, ); /// Replaces the top-most page of the page stack with the given one but treats @@ -112,8 +134,16 @@ extension GoRouterHelper on BuildContext { /// * [push] which pushes the given location onto the page stack. /// * [pushReplacement] which replaces the top-most page of the page stack but /// always uses a new page key. - void replace(String location, {Object? extra}) => - GoRouter.of(this).replace(location, extra: extra); + void replace( + String location, { + Object? extra, + Completer? onReplaceCompleter, + }) => + GoRouter.of(this).replace( + location, + extra: extra, + onReplaceCompleter: onReplaceCompleter, + ); /// Replaces the top-most page with the named route and optional parameters, /// preserving the page key. @@ -131,9 +161,13 @@ extension GoRouterHelper on BuildContext { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + Completer? onReplaceCompleter, }) => - GoRouter.of(this).replaceNamed(name, - pathParameters: pathParameters, - queryParameters: queryParameters, - extra: extra); + GoRouter.of(this).replaceNamed( + name, + pathParameters: pathParameters, + queryParameters: queryParameters, + extra: extra, + onReplaceCompleter: onReplaceCompleter, + ); } diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index b4115a1fca1..eda3a550519 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -125,6 +125,7 @@ class GoRouteInformationParser extends RouteInformationParser { baseRouteMatchList: state.baseRouteMatchList, completer: state.completer, type: state.type, + onReplaceCompleter: state.onReplaceCompleter, ); }); } @@ -182,6 +183,7 @@ class GoRouteInformationParser extends RouteInformationParser { required RouteMatchList? baseRouteMatchList, required Completer? completer, required NavigatingType type, + Completer? onReplaceCompleter, }) { switch (type) { case NavigatingType.push: @@ -190,24 +192,33 @@ class GoRouteInformationParser extends RouteInformationParser { pageKey: _getUniqueValueKey(), completer: completer!, matches: newMatchList, + onReplaceCompleter: onReplaceCompleter, ), ); case NavigatingType.pushReplacement: final RouteMatch routeMatch = baseRouteMatchList!.last; + if (routeMatch is ImperativeRouteMatch) { + routeMatch.onReplaceCompleter?.complete(); + } return baseRouteMatchList.remove(routeMatch).push( ImperativeRouteMatch( pageKey: _getUniqueValueKey(), completer: completer!, matches: newMatchList, + onReplaceCompleter: onReplaceCompleter, ), ); case NavigatingType.replace: final RouteMatch routeMatch = baseRouteMatchList!.last; + if (routeMatch is ImperativeRouteMatch) { + routeMatch.onReplaceCompleter?.complete(); + } return baseRouteMatchList.remove(routeMatch).push( ImperativeRouteMatch( pageKey: routeMatch.pageKey, completer: completer!, matches: newMatchList, + onReplaceCompleter: onReplaceCompleter, ), ); case NavigatingType.go: diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index dc6d88057eb..23cb46bbaee 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -373,12 +373,17 @@ class GoRouter implements RouterConfig { /// * [replace] which replaces the top-most page of the page stack but treats /// it as the same page. The page key will be reused. This will preserve the /// state and not run any page animation. - Future push(String location, {Object? extra}) async { + Future push( + String location, { + Object? extra, + Completer? onReplaceCompleter, + }) async { log('pushing $location'); return routeInformationProvider.push( location, base: routerDelegate.currentConfiguration, extra: extra, + onReplaceCompleter: onReplaceCompleter, ); } @@ -389,11 +394,13 @@ class GoRouter implements RouterConfig { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + Completer? onReplaceCompleter, }) => push( namedLocation(name, pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, + onReplaceCompleter: onReplaceCompleter, ); /// Replaces the top-most page of the page stack with the given URL location @@ -405,13 +412,17 @@ class GoRouter implements RouterConfig { /// * [replace] which replaces the top-most page of the page stack but treats /// it as the same page. The page key will be reused. This will preserve the /// state and not run any page animation. - Future pushReplacement(String location, - {Object? extra}) { + Future pushReplacement( + String location, { + Object? extra, + Completer? onReplaceCompleter, + }) { log('pushReplacement $location'); return routeInformationProvider.pushReplacement( location, base: routerDelegate.currentConfiguration, extra: extra, + onReplaceCompleter: onReplaceCompleter, ); } @@ -427,11 +438,13 @@ class GoRouter implements RouterConfig { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + Completer? onReplaceCompleter, }) { return pushReplacement( namedLocation(name, pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, + onReplaceCompleter: onReplaceCompleter, ); } @@ -445,12 +458,17 @@ class GoRouter implements RouterConfig { /// * [push] which pushes the given location onto the page stack. /// * [pushReplacement] which replaces the top-most page of the page stack but /// always uses a new page key. - Future replace(String location, {Object? extra}) { + Future replace( + String location, { + Object? extra, + Completer? onReplaceCompleter, + }) { log('replace $location'); return routeInformationProvider.replace( location, base: routerDelegate.currentConfiguration, extra: extra, + onReplaceCompleter: onReplaceCompleter, ); } @@ -470,11 +488,13 @@ class GoRouter implements RouterConfig { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + Completer? onReplaceCompleter, }) { return replace( namedLocation(name, pathParameters: pathParameters, queryParameters: queryParameters), extra: extra, + onReplaceCompleter: onReplaceCompleter, ); } diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 578b394be77..4d8331d16a6 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: 14.1.3 +version: 14.1.4 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/delegate_test.dart b/packages/go_router/test/delegate_test.dart index 6513a1c9665..30481edc393 100644 --- a/packages/go_router/test/delegate_test.dart +++ b/packages/go_router/test/delegate_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; @@ -355,6 +357,27 @@ void main() { ); }, ); + + testWidgets('It should complete onReplaceCompleter when replacing the page', + (WidgetTester tester) async { + final GoRouter goRouter = await createGoRouter(tester); + + goRouter.push('/page-0'); + await tester.pumpAndSettle(); + + final Completer completer = Completer(); + goRouter.push( + '/page-1', + onReplaceCompleter: completer, + ); + await tester.pumpAndSettle(); + + goRouter.pushReplacement('/page-2'); + await tester.pumpAndSettle(); + + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3); + expect(completer.isCompleted, true); + }); }); group('pushReplacementNamed', () { @@ -412,6 +435,50 @@ void main() { ); }, ); + + testWidgets('It should complete onReplaceCompleter when replacing the page', + (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()), + GoRoute( + path: '/page-2', + name: 'page2', + builder: (_, __) => const SizedBox()), + ], + ); + addTearDown(goRouter.dispose); + await tester.pumpWidget( + MaterialApp.router( + routerConfig: goRouter, + ), + ); + + goRouter.push('/page-0'); + await tester.pumpAndSettle(); + + final Completer completer = Completer(); + goRouter.push( + '/page-1', + onReplaceCompleter: completer, + ); + await tester.pumpAndSettle(); + + goRouter.pushReplacementNamed('page2'); + await tester.pumpAndSettle(); + + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3); + expect(completer.isCompleted, true); + }); }); group('replace', () { @@ -516,6 +583,27 @@ void main() { ); }, ); + + testWidgets('It should complete onReplaceCompleter when replacing the page', + (WidgetTester tester) async { + final GoRouter goRouter = await createGoRouter(tester); + + goRouter.push('/page-0'); + await tester.pumpAndSettle(); + + final Completer completer = Completer(); + goRouter.push( + '/page-1', + onReplaceCompleter: completer, + ); + await tester.pumpAndSettle(); + + goRouter.replace('/page-2'); + await tester.pumpAndSettle(); + + expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3); + expect(completer.isCompleted, true); + }); }); group('replaceNamed', () { diff --git a/packages/go_router/test/inherited_test.dart b/packages/go_router/test/inherited_test.dart index a5902d47dfe..93ebb0e9bc7 100644 --- a/packages/go_router/test/inherited_test.dart +++ b/packages/go_router/test/inherited_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -162,10 +164,13 @@ class MockGoRouter extends GoRouter { late String latestPushedName; @override - Future pushNamed(String name, - {Map pathParameters = const {}, - Map queryParameters = const {}, - Object? extra}) { + Future pushNamed( + String name, { + Map pathParameters = const {}, + Map queryParameters = const {}, + Object? extra, + Completer? onReplaceCompleter, + }) { latestPushedName = name; return Future.value(); } diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index e6f69c50788..32cd5d77071 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -4,6 +4,7 @@ // ignore_for_file: cascade_invocations, diagnostic_describe_all_properties +import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; @@ -110,7 +111,11 @@ class GoRouterPushSpy extends GoRouter { Object? extra; @override - Future push(String location, {Object? extra}) { + Future push( + String location, { + Object? extra, + Completer? onReplaceCompleter, + }) { myLocation = location; this.extra = extra; return Future.value(extra as T?); @@ -134,6 +139,7 @@ class GoRouterPushNamedSpy extends GoRouter { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + Completer? onReplaceCompleter, }) { this.name = name; this.pathParameters = pathParameters; From 6fb4b62b548519b409b7c0629c6fdb90c6a283f2 Mon Sep 17 00:00:00 2001 From: aleksanderb Date: Thu, 13 Jun 2024 12:46:20 +0200 Subject: [PATCH 2/2] [go_router]: Update pubspec --- packages/go_router/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 4d8331d16a6..09061001a77 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: 14.1.4 +version: 14.1.5 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