From ab4c7edb6680dfd8f6116cc3f847de33f80451aa Mon Sep 17 00:00:00 2001 From: nouvist <13176878+nouvist@users.noreply.github.com> Date: Fri, 13 Jun 2025 17:50:06 +0700 Subject: [PATCH] [go_router] Add MapRouteResultCallback and PipeRouteCompleterCallback Both are used to resolve the current route completer when it is replaced. Both are added to `RouteInformationState` and `ImperativeRouteMatch`. --- packages/go_router/CHANGELOG.md | 18 ++-- .../go_router/example/devtools_options.yaml | 3 + packages/go_router/lib/src/configuration.dart | 10 +- .../lib/src/information_provider.dart | 98 +++++++++++++++++-- packages/go_router/lib/src/match.dart | 15 ++- packages/go_router/lib/src/parser.dart | 23 ++++- packages/go_router/lib/src/router.dart | 49 ++++++++-- packages/go_router/pubspec.yaml | 2 +- .../go_router/test/imperative_api_test.dart | 70 +++++++++++++ packages/go_router/test/inherited_test.dart | 11 ++- packages/go_router/test/match_test.dart | 48 +++++++-- packages/go_router/test/matching_test.dart | 8 +- packages/go_router/test/test_helpers.dart | 7 +- 13 files changed, 313 insertions(+), 49 deletions(-) create mode 100644 packages/go_router/example/devtools_options.yaml diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 6518a538ed0..f50097499fd 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,10 @@ +## 15.3.0 + +- Resolves an issue where `replace` and `pushReplacement` caused the originating + route's completer to hang by adding `mapReplacementResult` to + `RouteInformationState` and `ImperativeRouteMatch`. + [flutter#141251](https://github.com/flutter/flutter/issues/141251) + ## 15.2.3 - Updates Type-safe routes topic documentation to use the mixin from `go_router_builder` 3.0.0. @@ -8,16 +15,16 @@ ## 15.2.1 -* Fixes Popping state and re-rendering scaffold at the same time doesn't update the URL on web. +- Fixes Popping state and re-rendering scaffold at the same time doesn't update the URL on web. ## 15.2.0 -* `GoRouteData` now defines `.location`, `.go(context)`, `.push(context)`, `.pushReplacement(context)`, and `replace(context)` to be used for [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html). **Requires go_router_builder >= 3.0.0**. +- `GoRouteData` now defines `.location`, `.go(context)`, `.push(context)`, `.pushReplacement(context)`, and `replace(context)` to be used for [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html). **Requires go_router_builder >= 3.0.0**. ## 15.1.3 -* Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. -* Fixes typo in API docs. +- Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. +- Fixes typo in API docs. ## 15.1.2 @@ -41,7 +48,7 @@ ## 14.8.1 - Secured canPop method for the lack of matches in routerDelegate's configuration. - + ## 14.8.0 - Adds `preload` parameter to `StatefulShellBranchData.$branch`. @@ -1205,4 +1212,3 @@ ## 0.1.0 - squatting on the package name (I'm not too proud to admit it) - diff --git a/packages/go_router/example/devtools_options.yaml b/packages/go_router/example/devtools_options.yaml new file mode 100644 index 00000000000..fa0b357c4f4 --- /dev/null +++ b/packages/go_router/example/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 95fd1002913..4aca9a5a30a 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -319,10 +319,12 @@ class RouteConfiguration { for (final ImperativeRouteMatch imperativeMatch in matchList.matches.whereType()) { final ImperativeRouteMatch match = ImperativeRouteMatch( - pageKey: imperativeMatch.pageKey, - matches: findMatch(imperativeMatch.matches.uri, - extra: imperativeMatch.matches.extra), - completer: imperativeMatch.completer); + pageKey: imperativeMatch.pageKey, + matches: findMatch(imperativeMatch.matches.uri, + extra: imperativeMatch.matches.extra), + completer: imperativeMatch.completer, + pipeCompleter: imperativeMatch.pipeCompleter, + ); result = result.push(match); } return result; diff --git a/packages/go_router/lib/src/information_provider.dart b/packages/go_router/lib/src/information_provider.dart index 400ead006f7..28863b0b898 100644 --- a/packages/go_router/lib/src/information_provider.dart +++ b/packages/go_router/lib/src/information_provider.dart @@ -36,6 +36,13 @@ enum NavigatingType { restore, } +/// Maps the replacement route result to the correct generic type. +typedef MapRouteResultCallback = T Function(Object? result); + +/// Pipe completer to the last route match completer. +typedef PipeRouteCompleterCallback = Completer + Function(Completer next); + /// The data class to be stored in [RouteInformation.state] to be used by /// [GoRouteInformationParser]. /// @@ -49,9 +56,16 @@ class RouteInformationState { this.completer, this.baseRouteMatchList, required this.type, + MapRouteResultCallback? mapReplacementResult, }) : assert((type == NavigatingType.go || type == NavigatingType.restore) == (completer == null)), - assert((type != NavigatingType.go) == (baseRouteMatchList != null)); + assert((type != NavigatingType.go) == (baseRouteMatchList != null)), + mapReplacementResult = + mapReplacementResult ?? _defaultMapReplacementResult; + + static T? _defaultMapReplacementResult(Object? result) { + return null; + } /// The extra object used when navigating with [GoRouter]. final Object? extra; @@ -63,6 +77,10 @@ class RouteInformationState { /// [NavigatingType.restore]. final Completer? completer; + /// Maps the replacement result to the appropriate type, to resolve the + /// [completer]. + final MapRouteResultCallback mapReplacementResult; + /// The base route match list to push on top to. /// /// This is only null if [type] is [NavigatingType.go]. @@ -70,6 +88,19 @@ class RouteInformationState { /// The type of navigation. final NavigatingType type; + + /// Pipes the completer to the next completer in the chain. + Completer pipeCompleter(Completer next) { + if (completer == null) { + return next; + } + + return _PipeCompleter( + next: next, + current: completer!, + mapReplacementResult: mapReplacementResult, + ); + } } /// The [RouteInformationProvider] created by go_router. @@ -156,8 +187,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, + MapRouteResultCallback? mapReplacementResult, + }) { final Completer completer = Completer(); _setValue( location, @@ -166,6 +201,7 @@ class GoRouteInformationProvider extends RouteInformationProvider baseRouteMatchList: base, completer: completer, type: NavigatingType.push, + mapReplacementResult: mapReplacementResult, ), ); return completer.future; @@ -196,8 +232,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, + MapRouteResultCallback? mapReplacementResult, + }) { final Completer completer = Completer(); _setValue( location, @@ -206,14 +246,19 @@ class GoRouteInformationProvider extends RouteInformationProvider baseRouteMatchList: base, completer: completer, type: NavigatingType.pushReplacement, + mapReplacementResult: mapReplacementResult, ), ); 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, + MapRouteResultCallback? mapReplacementResult, + }) { final Completer completer = Completer(); _setValue( location, @@ -290,3 +335,42 @@ class GoRouteInformationProvider extends RouteInformationProvider return SynchronousFuture(true); } } + +/// Ensures the replacement routes can resolve the originating route completer. +/// Mainly used by [RouteInformationState.pipeCompleter] +class _PipeCompleter + implements Completer { + _PipeCompleter({ + required Completer next, + required Completer current, + required MapRouteResultCallback mapReplacementResult, + }) : _next = next, + _current = current, + _mapReplacementResult = mapReplacementResult; + + final Completer _next; + final Completer _current; + final MapRouteResultCallback _mapReplacementResult; + + @override + void complete([FutureOr? value]) { + _next.complete(value); + if (!_current.isCompleted) { + _current.complete(_mapReplacementResult(value)); + } + } + + @override + void completeError(Object error, [StackTrace? stackTrace]) { + _next.completeError(error, stackTrace); + if (!_current.isCompleted) { + _current.completeError(error, stackTrace); + } + } + + @override + Future get future => _next.future; + + @override + bool get isCompleted => _next.isCompleted; +} diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart index bb4499dfaf3..37537fa51a5 100644 --- a/packages/go_router/lib/src/match.dart +++ b/packages/go_router/lib/src/match.dart @@ -12,6 +12,7 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart' as meta; import 'configuration.dart'; +import 'information_provider.dart'; import 'logging.dart'; import 'misc/errors.dart'; import 'path_utils.dart'; @@ -432,9 +433,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, + required this.pipeCompleter, + }) : super( route: _getsLastRouteFromMatches(matches), matchedLocation: _getsMatchedLocationFromMatches(matches), ); @@ -460,6 +464,9 @@ class ImperativeRouteMatch extends RouteMatch { /// The completer for the future returned by [GoRouter.push]. final Completer completer; + /// Pipes the completer to the next completer in the chain. + final PipeRouteCompleterCallback pipeCompleter; + /// Called when the corresponding [Route] associated with this route match is /// completed. void complete([dynamic value]) { @@ -967,6 +974,8 @@ class _RouteMatchListDecoder // https://github.com/flutter/flutter/issues/128122. completer: Completer(), matches: imperativeMatchList, + // TODO(nouvist): Sorry, I don't know what convert does. + pipeCompleter: (Completer next) => next, ); matchList = matchList.push(imperativeMatch); } diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index d1981898a8b..1aa681f60ca 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -117,6 +117,7 @@ class GoRouteInformationParser extends RouteInformationParser { baseRouteMatchList: state.baseRouteMatchList, completer: state.completer, type: state.type, + pipeCompleter: state.pipeCompleter, ); }); } @@ -168,11 +169,24 @@ class GoRouteInformationParser extends RouteInformationParser { return redirectedFuture; } + /// Ensures the replacement routes can resolve the originating route completer. + Completer _pipeCompleter( + Completer currentCompleter, + RouteMatch lastRouteMatch, + ) { + if (lastRouteMatch is ImperativeRouteMatch) { + return lastRouteMatch.pipeCompleter(currentCompleter); + } else { + return currentCompleter; + } + } + RouteMatchList _updateRouteMatchList( RouteMatchList newMatchList, { required RouteMatchList? baseRouteMatchList, required Completer? completer, required NavigatingType type, + required PipeRouteCompleterCallback pipeCompleter, }) { switch (type) { case NavigatingType.push: @@ -181,10 +195,12 @@ class GoRouteInformationParser extends RouteInformationParser { pageKey: _getUniqueValueKey(), completer: completer!, matches: newMatchList, + pipeCompleter: pipeCompleter, ), ); case NavigatingType.pushReplacement: final RouteMatch routeMatch = baseRouteMatchList!.last; + completer = _pipeCompleter(completer!, routeMatch); baseRouteMatchList = baseRouteMatchList.remove(routeMatch); if (baseRouteMatchList.isEmpty) { return newMatchList; @@ -192,12 +208,14 @@ class GoRouteInformationParser extends RouteInformationParser { return baseRouteMatchList.push( ImperativeRouteMatch( pageKey: _getUniqueValueKey(), - completer: completer!, + completer: completer, matches: newMatchList, + pipeCompleter: pipeCompleter, ), ); case NavigatingType.replace: final RouteMatch routeMatch = baseRouteMatchList!.last; + completer = _pipeCompleter(completer!, routeMatch); baseRouteMatchList = baseRouteMatchList.remove(routeMatch); if (baseRouteMatchList.isEmpty) { return newMatchList; @@ -205,8 +223,9 @@ class GoRouteInformationParser extends RouteInformationParser { return baseRouteMatchList.push( ImperativeRouteMatch( pageKey: routeMatch.pageKey, - completer: completer!, + completer: completer, matches: newMatchList, + pipeCompleter: pipeCompleter, ), ); case NavigatingType.go: diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 9962dd2d449..b06a32e6da1 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -387,12 +387,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, + MapRouteResultCallback? mapReplacementResult, + }) async { log('pushing $location'); return routeInformationProvider.push( location, base: routerDelegate.currentConfiguration, extra: extra, + mapReplacementResult: mapReplacementResult, ); } @@ -403,11 +408,16 @@ class GoRouter implements RouterConfig { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + MapRouteResultCallback? mapReplacementResult, }) => push( - namedLocation(name, - pathParameters: pathParameters, queryParameters: queryParameters), + namedLocation( + name, + pathParameters: pathParameters, + queryParameters: queryParameters, + ), extra: extra, + mapReplacementResult: mapReplacementResult, ); /// Replaces the top-most page of the page stack with the given URL location @@ -419,13 +429,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, + MapRouteResultCallback? mapReplacementResult, + }) { log('pushReplacement $location'); return routeInformationProvider.pushReplacement( location, base: routerDelegate.currentConfiguration, extra: extra, + mapReplacementResult: mapReplacementResult, ); } @@ -441,11 +455,16 @@ class GoRouter implements RouterConfig { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + MapRouteResultCallback? mapReplacementResult, }) { return pushReplacement( - namedLocation(name, - pathParameters: pathParameters, queryParameters: queryParameters), + namedLocation( + name, + pathParameters: pathParameters, + queryParameters: queryParameters, + ), extra: extra, + mapReplacementResult: mapReplacementResult, ); } @@ -459,12 +478,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, + MapRouteResultCallback? mapReplacementResult, + }) { log('replace $location'); return routeInformationProvider.replace( location, base: routerDelegate.currentConfiguration, extra: extra, + mapReplacementResult: mapReplacementResult, ); } @@ -484,11 +508,16 @@ class GoRouter implements RouterConfig { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + MapRouteResultCallback? mapReplacementResult, }) { return replace( - namedLocation(name, - pathParameters: pathParameters, queryParameters: queryParameters), + namedLocation( + name, + pathParameters: pathParameters, + queryParameters: queryParameters, + ), extra: extra, + mapReplacementResult: mapReplacementResult, ); } diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index c5d35eec684..dd279a67302 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: 15.2.3 +version: 15.3.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/imperative_api_test.dart b/packages/go_router/test/imperative_api_test.dart index 0598acf00bb..c026221bdef 100644 --- a/packages/go_router/test/imperative_api_test.dart +++ b/packages/go_router/test/imperative_api_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:math'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; @@ -294,4 +296,72 @@ void main() { expect(find.text('shell'), findsNothing); expect(find.byKey(e), findsOneWidget); }); + + testWidgets('completer able to be passed on replace', + (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/initial', + builder: (_, __) => const DummyScreen(), + ), + GoRoute( + path: '/intermediate', + builder: (_, __) => const DummyScreen(), + ), + GoRoute( + path: '/final', + builder: (_, __) => const DummyScreen(), + ), + ]; + + final GoRouter router = await createRouter( + routes, + tester, + initialLocation: '/initial', + ); + + final Random random = Random(); + + /** resolve to null by default **/ { + final int result = random.nextInt(1000); + final Future push = router.push('/intermediate'); + await tester.pumpAndSettle(); + expect(router.state.path, '/intermediate'); + + final Future replace = router.replace('/final'); + await tester.pumpAndSettle(); + expect(router.state.path, '/final'); + + router.pop(result); + await tester.pumpAndSettle(); + expect(router.state.path, '/initial'); + await tester.pumpAndSettle(); + + await expectLater(push, completion(null)); + await expectLater(replace, completion(result)); + } + + /** map and resolve **/ { + final int result = random.nextInt(1000); + final Future push = router.push( + '/intermediate', + mapReplacementResult: (Object? result) => + result is num ? result.toString() : null, + ); + await tester.pumpAndSettle(); + expect(router.state.path, '/intermediate'); + + final Future replace = router.replace('/final'); + await tester.pumpAndSettle(); + expect(router.state.path, '/final'); + + router.pop(result); + await tester.pumpAndSettle(); + expect(router.state.path, '/initial'); + await tester.pumpAndSettle(); + + await expectLater(push, completion(result.toString())); + await expectLater(replace, completion(result)); + } + }); } diff --git a/packages/go_router/test/inherited_test.dart b/packages/go_router/test/inherited_test.dart index a5902d47dfe..d1a0af28d18 100644 --- a/packages/go_router/test/inherited_test.dart +++ b/packages/go_router/test/inherited_test.dart @@ -162,10 +162,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, + MapRouteResultCallback? mapReplacementResult, + }) { latestPushedName = name; return Future.value(); } diff --git a/packages/go_router/test/match_test.dart b/packages/go_router/test/match_test.dart index 57951b2ae23..e134074bfcb 100644 --- a/packages/go_router/test/match_test.dart +++ b/packages/go_router/test/match_test.dart @@ -223,30 +223,62 @@ void main() { test('can equal and has', () async { ImperativeRouteMatch match1 = ImperativeRouteMatch( - pageKey: key1, matches: matchList1, completer: completer1); + pageKey: key1, + matches: matchList1, + completer: completer1, + pipeCompleter: (Completer next) => next, + ); ImperativeRouteMatch match2 = ImperativeRouteMatch( - pageKey: key1, matches: matchList1, completer: completer1); + pageKey: key1, + matches: matchList1, + completer: completer1, + pipeCompleter: (Completer next) => next, + ); expect(match1 == match2, isTrue); expect(match1.hashCode == match2.hashCode, isTrue); match1 = ImperativeRouteMatch( - pageKey: key1, matches: matchList1, completer: completer1); + pageKey: key1, + matches: matchList1, + completer: completer1, + pipeCompleter: (Completer next) => next, + ); match2 = ImperativeRouteMatch( - pageKey: key2, matches: matchList1, completer: completer1); + pageKey: key2, + matches: matchList1, + completer: completer1, + pipeCompleter: (Completer next) => next, + ); expect(match1 == match2, isFalse); expect(match1.hashCode == match2.hashCode, isFalse); match1 = ImperativeRouteMatch( - pageKey: key1, matches: matchList1, completer: completer1); + pageKey: key1, + matches: matchList1, + completer: completer1, + pipeCompleter: (Completer next) => next, + ); match2 = ImperativeRouteMatch( - pageKey: key1, matches: matchList2, completer: completer1); + pageKey: key1, + matches: matchList2, + completer: completer1, + pipeCompleter: (Completer next) => next, + ); expect(match1 == match2, isFalse); expect(match1.hashCode == match2.hashCode, isFalse); match1 = ImperativeRouteMatch( - pageKey: key1, matches: matchList1, completer: completer1); + pageKey: key1, + matches: matchList1, + completer: completer1, + pipeCompleter: (Completer next) => next, + ); match2 = ImperativeRouteMatch( - pageKey: key1, matches: matchList1, completer: completer2); + pageKey: key1, + matches: matchList1, + completer: completer2, + pipeCompleter: (Completer next) => next, + ); expect(match1 == match2, isFalse); expect(match1.hashCode == match2.hashCode, isFalse); }); diff --git a/packages/go_router/test/matching_test.dart b/packages/go_router/test/matching_test.dart index f2dffbd3de1..0177066a81b 100644 --- a/packages/go_router/test/matching_test.dart +++ b/packages/go_router/test/matching_test.dart @@ -95,9 +95,11 @@ void main() { final RouteMatchList list1 = configuration.findMatch(Uri.parse('/a')); final RouteMatchList list2 = configuration.findMatch(Uri.parse('/b')); list1.push(ImperativeRouteMatch( - pageKey: const ValueKey('/b-p0'), - matches: list2, - completer: Completer())); + pageKey: const ValueKey('/b-p0'), + matches: list2, + completer: Completer(), + pipeCompleter: (Completer next) => next, + )); final Map encoded = codec.encode(list1); final RouteMatchList decoded = codec.decode(encoded); diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index cb1c2acdb96..1ea858be3ad 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -114,7 +114,11 @@ class GoRouterPushSpy extends GoRouter { Object? extra; @override - Future push(String location, {Object? extra}) { + Future push( + String location, { + Object? extra, + MapRouteResultCallback? mapReplacementResult, + }) { myLocation = location; this.extra = extra; return Future.value(extra as T?); @@ -138,6 +142,7 @@ class GoRouterPushNamedSpy extends GoRouter { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + MapRouteResultCallback? mapReplacementResult, }) { this.name = name; this.pathParameters = pathParameters;