Return value when pop (#3368)

Return value when pop
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 87ec4b5..e8c655b 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 6.5.0
+
+- Supports returning values on pop.
+
 ## 6.4.1
 - Adds `initialExtra` to **GoRouter** to pass extra data alongside `initialRoute`.
 
@@ -22,6 +26,7 @@
 
 - Adds `GoRouter.maybeOf` to get the closest `GoRouter` from the context, if there is any.
 
+
 ## 6.0.10
 
 - Adds helpers for go_router_builder for ShellRoute support
diff --git a/packages/go_router/doc/navigation.md b/packages/go_router/doc/navigation.md
index 93d51ed..f5fa16a 100644
--- a/packages/go_router/doc/navigation.md
+++ b/packages/go_router/doc/navigation.md
@@ -68,4 +68,21 @@
 );
 ```
 
+## Returning values
+Waiting for a value to be returned:
+
+```dart
+onTap: () {
+  final bool? result = await context.push<bool>('/page2');
+  if(result ?? false)...
+}
+```
+
+Returning a value:
+
+```dart
+onTap: () => context.pop(true)
+```
+
+
 [Named routes]: https://pub.dev/documentation/go_router/latest/topics/Named%20routes-topic.html
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 9440266..7c4c6e3 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -82,8 +82,9 @@
     return ValueKey<String>('$path-p$count');
   }
 
-  void _push(RouteMatchList matches, ValueKey<String> pageKey) {
-    final ImperativeRouteMatch newPageKeyMatch = ImperativeRouteMatch(
+  Future<T?> _push<T extends Object?>(
+      RouteMatchList matches, ValueKey<String> pageKey) async {
+    final ImperativeRouteMatch<T> newPageKeyMatch = ImperativeRouteMatch<T>(
       route: matches.last.route,
       subloc: matches.last.subloc,
       extra: matches.last.extra,
@@ -93,6 +94,7 @@
     );
 
     _matchList.push(newPageKeyMatch);
+    return newPageKeyMatch._future;
   }
 
   /// Pushes the given location onto the page stack.
@@ -103,12 +105,13 @@
   /// * [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 push(RouteMatchList matches) {
+  Future<T?> push<T extends Object?>(RouteMatchList matches) async {
     assert(matches.last.route is! ShellRoute);
 
     final ValueKey<String> pageKey = _getNewKeyForPath(matches.fullpath);
-    _push(matches, pageKey);
+    final Future<T?> future = _push(matches, pageKey);
     notifyListeners();
+    return future;
   }
 
   /// Returns `true` if the active Navigator can pop.
@@ -127,6 +130,7 @@
     final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
     while (iterator.moveNext()) {
       if (iterator.current.canPop()) {
+        iterator.matchList.last.complete(result);
         iterator.current.pop<T>(result);
         return;
       }
@@ -308,7 +312,7 @@
 
 /// The route match that represent route pushed through [GoRouter.push].
 // TODO(chunhtai): Removes this once imperative API no longer insert route match.
-class ImperativeRouteMatch extends RouteMatch {
+class ImperativeRouteMatch<T> extends RouteMatch {
   /// Constructor for [ImperativeRouteMatch].
   ImperativeRouteMatch({
     required super.route,
@@ -317,8 +321,20 @@
     required super.error,
     required super.pageKey,
     required this.matches,
-  });
+  }) : _completer = Completer<T?>();
 
   /// The matches that produces this route match.
   final RouteMatchList matches;
+
+  /// The completer for the future returned by [GoRouter.push].
+  final Completer<T?> _completer;
+
+  @override
+  void complete([dynamic value]) {
+    _completer.complete(value as T?);
+  }
+
+  /// The future of the [RouteMatch] completer.
+  /// When the future completes, this will return the value passed to [complete].
+  Future<T?> get _future => _completer.future;
 }
diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart
index 62a4d55..f4840f2 100644
--- a/packages/go_router/lib/src/match.dart
+++ b/packages/go_router/lib/src/match.dart
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flutter/foundation.dart';
 import 'package:flutter/widgets.dart';
 
 import 'matching.dart';
@@ -61,6 +60,9 @@
     throw MatcherError('Unexpected route type: $route', restLoc);
   }
 
+  /// Called when the corresponding [Route] associated with this route match is completed.
+  void complete([Object? value]) {}
+
   /// The matched route.
   final RouteBase route;
 
diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart
index 0900588..47f5b08 100644
--- a/packages/go_router/lib/src/misc/extensions.dart
+++ b/packages/go_router/lib/src/misc/extensions.dart
@@ -44,17 +44,17 @@
   /// * [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 push(String location, {Object? extra}) =>
-      GoRouter.of(this).push(location, extra: extra);
+  Future<T?> push<T extends Object?>(String location, {Object? extra}) =>
+      GoRouter.of(this).push<T>(location, extra: extra);
 
   /// Navigate to a named route onto the page stack.
-  void pushNamed(
+  Future<T?> pushNamed<T extends Object?>(
     String name, {
     Map<String, String> params = const <String, String>{},
     Map<String, dynamic> queryParams = const <String, dynamic>{},
     Object? extra,
   }) =>
-      GoRouter.of(this).pushNamed(
+      GoRouter.of(this).pushNamed<T>(
         name,
         params: params,
         queryParams: queryParams,
diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart
index e91eaf3..37c3e98 100644
--- a/packages/go_router/lib/src/parser.dart
+++ b/packages/go_router/lib/src/parser.dart
@@ -104,7 +104,7 @@
     }
     if (configuration.matches.last is ImperativeRouteMatch) {
       configuration =
-          (configuration.matches.last as ImperativeRouteMatch).matches;
+          (configuration.matches.last as ImperativeRouteMatch<Object?>).matches;
     }
     return RouteInformation(
       location: configuration.uri.toString(),
diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart
index 2960372..b63cb99 100644
--- a/packages/go_router/lib/src/router.dart
+++ b/packages/go_router/lib/src/router.dart
@@ -159,7 +159,7 @@
         routerDelegate.currentConfiguration.matches.last
             is ImperativeRouteMatch) {
       newLocation = (routerDelegate.currentConfiguration.matches.last
-              as ImperativeRouteMatch)
+              as ImperativeRouteMatch<Object?>)
           .matches
           .uri
           .toString();
@@ -219,32 +219,31 @@
   /// * [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 push(String location, {Object? extra}) {
+  Future<T?> push<T extends Object?>(String location, {Object? extra}) async {
     assert(() {
       log.info('pushing $location');
       return true;
     }());
-    _routeInformationParser
-        .parseRouteInformationWithDependencies(
+    final RouteMatchList matches =
+        await _routeInformationParser.parseRouteInformationWithDependencies(
       RouteInformation(location: location, state: extra),
       // TODO(chunhtai): avoid accessing the context directly through global key.
       // https://github.com/flutter/flutter/issues/99112
       _routerDelegate.navigatorKey.currentContext!,
-    )
-        .then<void>((RouteMatchList matches) {
-      _routerDelegate.push(matches);
-    });
+    );
+
+    return _routerDelegate.push<T>(matches);
   }
 
   /// Push a named route onto the page stack w/ optional parameters, e.g.
   /// `name='person', params={'fid': 'f2', 'pid': 'p1'}`
-  void pushNamed(
+  Future<T?> pushNamed<T extends Object?>(
     String name, {
     Map<String, String> params = const <String, String>{},
     Map<String, dynamic> queryParams = const <String, dynamic>{},
     Object? extra,
   }) =>
-      push(
+      push<T>(
         namedLocation(name, params: params, queryParams: queryParams),
         extra: extra,
       );
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index d4d4b29..991a4f9 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: 6.4.1
+version: 6.5.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/delegate_test.dart b/packages/go_router/test/delegate_test.dart
index b6cc6bc..e40084a 100644
--- a/packages/go_router/test/delegate_test.dart
+++ b/packages/go_router/test/delegate_test.dart
@@ -141,7 +141,7 @@
         reason: 'The last match should have been removed',
       );
       expect(
-        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch)
+        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
             .matches
             .uri
             .toString(),
@@ -270,7 +270,7 @@
         reason: 'The last match should have been removed',
       );
       expect(
-        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch)
+        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
             .matches
             .uri
             .toString(),
@@ -393,7 +393,7 @@
         reason: 'The last match should have been removed',
       );
       expect(
-        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch)
+        (goRouter.routerDelegate.matches.last as ImperativeRouteMatch<Object?>)
             .matches
             .uri
             .toString(),
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index 1733513..833c16e 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -2439,8 +2439,8 @@
       expect(router.location, loc);
       expect(matches.matches, hasLength(2));
       expect(find.byType(PersonScreen), findsOneWidget);
-      final ImperativeRouteMatch imperativeRouteMatch =
-          matches.matches.last as ImperativeRouteMatch;
+      final ImperativeRouteMatch<Object?> imperativeRouteMatch =
+          matches.matches.last as ImperativeRouteMatch<Object?>;
       expect(imperativeRouteMatch.matches.pathParameters['fid'], fid);
       expect(imperativeRouteMatch.matches.pathParameters['pid'], pid);
     });
@@ -2698,6 +2698,26 @@
       expect(router.extra, extra);
     });
 
+    testWidgets('calls [push] on closest GoRouter and waits for result',
+        (WidgetTester tester) async {
+      final GoRouterPushSpy router = GoRouterPushSpy(routes: routes);
+      await tester.pumpWidget(
+        MaterialApp.router(
+          routeInformationProvider: router.routeInformationProvider,
+          routeInformationParser: router.routeInformationParser,
+          routerDelegate: router.routerDelegate,
+          title: 'GoRouter Example',
+        ),
+      );
+      final String? result = await router.push<String>(
+        location,
+        extra: extra,
+      );
+      expect(result, extra);
+      expect(router.myLocation, location);
+      expect(router.extra, extra);
+    });
+
     testWidgets('calls [pushNamed] on closest GoRouter',
         (WidgetTester tester) async {
       final GoRouterPushNamedSpy router = GoRouterPushNamedSpy(routes: routes);
@@ -2719,6 +2739,30 @@
       expect(router.extra, extra);
     });
 
+    testWidgets('calls [pushNamed] on closest GoRouter and waits for result',
+        (WidgetTester tester) async {
+      final GoRouterPushNamedSpy router = GoRouterPushNamedSpy(routes: routes);
+      await tester.pumpWidget(
+        MaterialApp.router(
+          routeInformationProvider: router.routeInformationProvider,
+          routeInformationParser: router.routeInformationParser,
+          routerDelegate: router.routerDelegate,
+          title: 'GoRouter Example',
+        ),
+      );
+      final String? result = await router.pushNamed<String>(
+        name,
+        params: params,
+        queryParams: queryParams,
+        extra: extra,
+      );
+      expect(result, extra);
+      expect(router.extra, extra);
+      expect(router.name, name);
+      expect(router.params, params);
+      expect(router.queryParams, queryParams);
+    });
+
     testWidgets('calls [pop] on closest GoRouter', (WidgetTester tester) async {
       final GoRouterPopSpy router = GoRouterPopSpy(routes: routes);
       await tester.pumpWidget(
diff --git a/packages/go_router/test/inherited_test.dart b/packages/go_router/test/inherited_test.dart
index 9fbb915..7afa9fe 100644
--- a/packages/go_router/test/inherited_test.dart
+++ b/packages/go_router/test/inherited_test.dart
@@ -129,11 +129,12 @@
   late String latestPushedName;
 
   @override
-  void pushNamed(String name,
+  Future<T?> pushNamed<T extends Object?>(String name,
       {Map<String, String> params = const <String, String>{},
       Map<String, dynamic> queryParams = const <String, dynamic>{},
       Object? extra}) {
     latestPushedName = name;
+    return Future<T?>.value();
   }
 
   @override
diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart
index 04d12e7..b81f4c2 100644
--- a/packages/go_router/test/test_helpers.dart
+++ b/packages/go_router/test/test_helpers.dart
@@ -95,9 +95,10 @@
   Object? extra;
 
   @override
-  void push(String location, {Object? extra}) {
+  Future<T?> push<T extends Object?>(String location, {Object? extra}) {
     myLocation = location;
     this.extra = extra;
+    return Future<T?>.value(extra as T?);
   }
 }
 
@@ -110,7 +111,7 @@
   Object? extra;
 
   @override
-  void pushNamed(
+  Future<T?> pushNamed<T extends Object?>(
     String name, {
     Map<String, String> params = const <String, String>{},
     Map<String, dynamic> queryParams = const <String, dynamic>{},
@@ -120,6 +121,7 @@
     this.params = params;
     this.queryParams = queryParams;
     this.extra = extra;
+    return Future<T?>.value(extra as T?);
   }
 }