[go_router] Refactors GoRouter.pop to handle pageless route (#2879)

* Refactors GoRouter.pop to handle pageless route

* update
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 333e564..ff7402e 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 5.2.1
+
+- Refactors `GoRouter.pop` to be able to pop individual pageless route with result.
+
 ## 5.2.0
 
 - Fixes `GoRouterState.location` and `GoRouterState.param` to return correct value.
diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart
index 42989d7..04c6656 100644
--- a/packages/go_router/lib/src/builder.dart
+++ b/packages/go_router/lib/src/builder.dart
@@ -54,7 +54,7 @@
   Widget build(
     BuildContext context,
     RouteMatchList matchList,
-    VoidCallback pop,
+    PopPageCallback onPopPage,
     bool routerNeglect,
   ) {
     if (matchList.isEmpty) {
@@ -69,14 +69,14 @@
           try {
             final Map<Page<Object?>, GoRouterState> newRegistry =
                 <Page<Object?>, GoRouterState>{};
-            final Widget result = tryBuild(context, matchList, pop,
+            final Widget result = tryBuild(context, matchList, onPopPage,
                 routerNeglect, configuration.navigatorKey, newRegistry);
             _registry.updateRegistry(newRegistry);
             return GoRouterStateRegistryScope(
                 registry: _registry, child: result);
           } on _RouteBuilderError catch (e) {
-            return _buildErrorNavigator(
-                context, e, matchList.uri, pop, configuration.navigatorKey);
+            return _buildErrorNavigator(context, e, matchList.uri, onPopPage,
+                configuration.navigatorKey);
           }
         },
       ),
@@ -91,7 +91,7 @@
   Widget tryBuild(
     BuildContext context,
     RouteMatchList matchList,
-    VoidCallback pop,
+    PopPageCallback onPopPage,
     bool routerNeglect,
     GlobalKey<NavigatorState> navigatorKey,
     Map<Page<Object?>, GoRouterState> registry,
@@ -99,9 +99,9 @@
     return builderWithNav(
       context,
       _buildNavigator(
-        pop,
-        buildPages(
-            context, matchList, pop, routerNeglect, navigatorKey, registry),
+        onPopPage,
+        buildPages(context, matchList, onPopPage, routerNeglect, navigatorKey,
+            registry),
         navigatorKey,
         observers: observers,
       ),
@@ -114,15 +114,15 @@
   List<Page<Object?>> buildPages(
       BuildContext context,
       RouteMatchList matchList,
-      VoidCallback onPop,
+      PopPageCallback onPopPage,
       bool routerNeglect,
       GlobalKey<NavigatorState> navigatorKey,
       Map<Page<Object?>, GoRouterState> registry) {
     try {
       final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
           <GlobalKey<NavigatorState>, List<Page<Object?>>>{};
-      _buildRecursive(context, matchList, 0, onPop, routerNeglect, keyToPage,
-          navigatorKey, registry);
+      _buildRecursive(context, matchList, 0, onPopPage, routerNeglect,
+          keyToPage, navigatorKey, registry);
       return keyToPage[navigatorKey]!;
     } on _RouteBuilderError catch (e) {
       return <Page<Object?>>[
@@ -135,7 +135,7 @@
     BuildContext context,
     RouteMatchList matchList,
     int startIndex,
-    VoidCallback pop,
+    PopPageCallback onPopPage,
     bool routerNeglect,
     Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPages,
     GlobalKey<NavigatorState> navigatorKey,
@@ -163,8 +163,8 @@
 
       keyToPages.putIfAbsent(goRouteNavKey, () => <Page<Object?>>[]).add(page);
 
-      _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect,
-          keyToPages, navigatorKey, registry);
+      _buildRecursive(context, matchList, startIndex + 1, onPopPage,
+          routerNeglect, keyToPages, navigatorKey, registry);
     } else if (route is ShellRoute) {
       // The key for the Navigator that will display this ShellRoute's page.
       final GlobalKey<NavigatorState> parentNavigatorKey = navigatorKey;
@@ -184,12 +184,12 @@
       final int shellPageIdx = keyToPages[parentNavigatorKey]!.length;
 
       // Build the remaining pages
-      _buildRecursive(context, matchList, startIndex + 1, pop, routerNeglect,
-          keyToPages, shellNavigatorKey, registry);
+      _buildRecursive(context, matchList, startIndex + 1, onPopPage,
+          routerNeglect, keyToPages, shellNavigatorKey, registry);
 
       // Build the Navigator
       final Widget child = _buildNavigator(
-          pop, keyToPages[shellNavigatorKey]!, shellNavigatorKey);
+          onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey);
 
       // Build the Page for this route
       final Page<Object?> page =
@@ -203,7 +203,7 @@
   }
 
   Navigator _buildNavigator(
-    VoidCallback pop,
+    PopPageCallback onPopPage,
     List<Page<Object?>> pages,
     Key? navigatorKey, {
     List<NavigatorObserver> observers = const <NavigatorObserver>[],
@@ -213,13 +213,7 @@
       restorationScopeId: restorationScopeId,
       pages: pages,
       observers: observers,
-      onPopPage: (Route<dynamic> route, dynamic result) {
-        if (!route.didPop(result)) {
-          return false;
-        }
-        pop();
-        return true;
-      },
+      onPopPage: onPopPage,
     );
   }
 
@@ -393,10 +387,14 @@
       );
 
   /// Builds a Navigator containing an error page.
-  Widget _buildErrorNavigator(BuildContext context, _RouteBuilderError e,
-      Uri uri, VoidCallback pop, GlobalKey<NavigatorState> navigatorKey) {
+  Widget _buildErrorNavigator(
+      BuildContext context,
+      _RouteBuilderError e,
+      Uri uri,
+      PopPageCallback onPopPage,
+      GlobalKey<NavigatorState> navigatorKey) {
     return _buildNavigator(
-      pop,
+      onPopPage,
       <Page<Object?>>[
         _buildErrorPage(context, e, uri),
       ],
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 2ccbd4c..3bb303c 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -11,6 +11,7 @@
 import 'configuration.dart';
 import 'match.dart';
 import 'matching.dart';
+import 'misc/errors.dart';
 import 'typedefs.dart';
 
 /// GoRouter implementation of [RouterDelegate].
@@ -59,38 +60,19 @@
   final Map<String, int> _pushCounts = <String, int>{};
   final RouteConfiguration _configuration;
 
+  _NavigatorStateIterator _createNavigatorStateIterator() =>
+      _NavigatorStateIterator(_matchList, navigatorKey.currentState!);
+
   @override
   Future<bool> popRoute() async {
-    // Iterate backwards through the RouteMatchList until seeing a GoRoute with
-    // a non-null parentNavigatorKey or a ShellRoute with a non-null
-    // parentNavigatorKey and pop from that Navigator instead of the root.
-    final int matchCount = _matchList.matches.length;
-    for (int i = matchCount - 1; i >= 0; i -= 1) {
-      final RouteMatch match = _matchList.matches[i];
-      final RouteBase route = match.route;
-
-      if (route is GoRoute && route.parentNavigatorKey != null) {
-        final bool didPop =
-            await route.parentNavigatorKey!.currentState!.maybePop();
-
-        // Continue if didPop was false.
-        if (didPop) {
-          return didPop;
-        }
-      } else if (route is ShellRoute) {
-        final bool didPop = await route.navigatorKey.currentState!.maybePop();
-
-        // Continue if didPop was false.
-        if (didPop) {
-          return didPop;
-        }
+    final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
+    while (iterator.moveNext()) {
+      final bool didPop = await iterator.current.maybePop();
+      if (didPop) {
+        return true;
       }
     }
-
-    // Use the root navigator if no ShellRoute Navigators were found and didn't
-    // pop
-    final NavigatorState navigator = navigatorKey.currentState!;
-    return navigator.maybePop();
+    return false;
   }
 
   /// Pushes the given location onto the page stack
@@ -117,29 +99,25 @@
 
   /// Returns `true` if the active Navigator can pop.
   bool canPop() {
-    // Loop through navigators in reverse and call canPop()
-    final int matchCount = _matchList.matches.length;
-    for (int i = matchCount - 1; i >= 0; i -= 1) {
-      final RouteMatch match = _matchList.matches[i];
-      final RouteBase route = match.route;
-      if (route is GoRoute && route.parentNavigatorKey != null) {
-        final bool canPop =
-            route.parentNavigatorKey!.currentState?.canPop() ?? false;
-
-        // Continue if canPop is false.
-        if (canPop) {
-          return canPop;
-        }
-      } else if (route is ShellRoute) {
-        final bool canPop = route.navigatorKey.currentState?.canPop() ?? false;
-
-        // Continue if canPop is false.
-        if (canPop) {
-          return canPop;
-        }
+    final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
+    while (iterator.moveNext()) {
+      if (iterator.current.canPop()) {
+        return true;
       }
     }
-    return navigatorKey.currentState?.canPop() ?? false;
+    return false;
+  }
+
+  /// Pops the top-most route.
+  void pop<T extends Object?>([T? result]) {
+    final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
+    while (iterator.moveNext()) {
+      if (iterator.current.canPop()) {
+        iterator.current.pop<T>(result);
+        return;
+      }
+    }
+    throw GoError('There is nothing to pop');
   }
 
   void _debugAssertMatchListNotEmpty() {
@@ -150,14 +128,16 @@
     );
   }
 
-  /// Pop the top page off the GoRouter's page stack.
-  void pop() {
+  bool _onPopPage(Route<Object?> route, Object? result) {
+    if (!route.didPop(result)) {
+      return false;
+    }
     _matchList.pop();
     assert(() {
       _debugAssertMatchListNotEmpty();
       return true;
     }());
-    notifyListeners();
+    return true;
   }
 
   /// Replaces the top-most page of the page stack with the given one.
@@ -187,7 +167,7 @@
     return builder.build(
       context,
       _matchList,
-      pop,
+      _onPopPage,
       routerNeglect,
     );
   }
@@ -204,6 +184,84 @@
   }
 }
 
+/// An iterator that iterates through navigators that [GoRouterDelegate]
+/// created from the inner to outer.
+///
+/// The iterator starts with the navigator that hosts the top-most route. This
+/// navigator may not be the inner-most navigator if the top-most route is a
+/// pageless route, such as a dialog or bottom sheet.
+class _NavigatorStateIterator extends Iterator<NavigatorState> {
+  _NavigatorStateIterator(this.matchList, this.root)
+      : index = matchList.matches.length;
+
+  final RouteMatchList matchList;
+  int index = 0;
+  final NavigatorState root;
+  @override
+  late NavigatorState current;
+
+  @override
+  bool moveNext() {
+    if (index < 0) {
+      return false;
+    }
+    for (index -= 1; index >= 0; index -= 1) {
+      final RouteMatch match = matchList.matches[index];
+      final RouteBase route = match.route;
+      if (route is GoRoute && route.parentNavigatorKey != null) {
+        final GlobalKey<NavigatorState> parentNavigatorKey =
+            route.parentNavigatorKey!;
+        final ModalRoute<Object?>? parentModalRoute =
+            ModalRoute.of(parentNavigatorKey.currentContext!);
+        // The ModalRoute can be null if the parentNavigatorKey references the
+        // root navigator.
+        if (parentModalRoute == null) {
+          index = -1;
+          assert(root == parentNavigatorKey.currentState);
+          current = root;
+          return true;
+        }
+        // It must be a ShellRoute that holds this parentNavigatorKey;
+        // otherwise, parentModalRoute would have been null. Updates the index
+        // to the ShellRoute
+        for (index -= 1; index >= 0; index -= 1) {
+          final RouteBase route = matchList.matches[index].route;
+          if (route is ShellRoute) {
+            if (route.navigatorKey == parentNavigatorKey) {
+              break;
+            }
+          }
+        }
+        // There may be a pageless route on top of ModalRoute that the
+        // NavigatorState of parentNavigatorKey is in. For example, an open
+        // dialog. In that case we want to find the navigator that host the
+        // pageless route.
+        if (parentModalRoute.isCurrent == false) {
+          continue;
+        }
+
+        current = parentNavigatorKey.currentState!;
+        return true;
+      } else if (route is ShellRoute) {
+        // Must have a ModalRoute parent because the navigator ShellRoute
+        // created must not be the root navigator.
+        final ModalRoute<Object?> parentModalRoute =
+            ModalRoute.of(route.navigatorKey.currentContext!)!;
+        // There may be pageless route on top of ModalRoute that the
+        // parentNavigatorKey is in. For example an open dialog.
+        if (parentModalRoute.isCurrent == false) {
+          continue;
+        }
+        current = route.navigatorKey.currentState!;
+        return true;
+      }
+    }
+    assert(index == -1);
+    current = root;
+    return true;
+  }
+}
+
 /// 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 {
diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart
index 120bfd2..548747e 100644
--- a/packages/go_router/lib/src/router.dart
+++ b/packages/go_router/lib/src/router.dart
@@ -142,7 +142,7 @@
   String get location => _location;
   String _location = '/';
 
-  /// Returns `true` if there is more than 1 page on the stack.
+  /// Returns `true` if there is at least two or more route can be pop.
   bool canPop() => _routerDelegate.canPop();
 
   void _handleStateMayChange() {
@@ -272,13 +272,16 @@
     );
   }
 
-  /// Pop the top page off the GoRouter's page stack.
-  void pop() {
+  /// Pop the top-most route off the current screen.
+  ///
+  /// If the top-most route is a pop up or dialog, this method pops it instead
+  /// of any GoRoute under it.
+  void pop<T extends Object?>([T? result]) {
     assert(() {
       log.info('popping $location');
       return true;
     }());
-    _routerDelegate.pop();
+    _routerDelegate.pop<T>(result);
   }
 
   /// Refresh the route.
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index 5655bd6..64eb598 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: 5.2.0
+version: 5.2.1
 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/builder_test.dart b/packages/go_router/test/builder_test.dart
index ba4f7f5..04e7543 100644
--- a/packages/go_router/test/builder_test.dart
+++ b/packages/go_router/test/builder_test.dart
@@ -346,7 +346,7 @@
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
-      home: builder.tryBuild(context, matches, () {}, false,
+      home: builder.tryBuild(context, matches, (_, __) => false, false,
           routeConfiguration.navigatorKey, <Page<Object?>, GoRouterState>{}),
       // builder: (context, child) => ,
     );
diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart
index 3974223..7df8504 100644
--- a/packages/go_router/test/delegate_test.dart
+++ b/packages/go_router/test/delegate_test.dart
@@ -36,24 +36,21 @@
     testWidgets('removes the last element', (WidgetTester tester) async {
       final GoRouter goRouter = await createGoRouter(tester)
         ..push('/error');
+      await tester.pumpAndSettle();
 
-      goRouter.routerDelegate.addListener(expectAsync0(() {}));
       final RouteMatch last = goRouter.routerDelegate.matches.matches.last;
-      goRouter.routerDelegate.pop();
+      await goRouter.routerDelegate.popRoute();
       expect(goRouter.routerDelegate.matches.matches.length, 1);
       expect(goRouter.routerDelegate.matches.matches.contains(last), false);
     });
 
-    testWidgets('throws when it pops more than matches count',
+    testWidgets('pops more than matches count should return false',
         (WidgetTester tester) async {
       final GoRouter goRouter = await createGoRouter(tester)
         ..push('/error');
-      expect(
-        () => goRouter.routerDelegate
-          ..pop()
-          ..pop(),
-        throwsA(isAssertionError),
-      );
+      await tester.pumpAndSettle();
+      await goRouter.routerDelegate.popRoute();
+      expect(await goRouter.routerDelegate.popRoute(), isFalse);
     });
   });
 
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index d301742..35198e0 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -2549,15 +2549,6 @@
   });
 
   group('Imperative navigation', () {
-    testWidgets('pop triggers pop on routerDelegate',
-        (WidgetTester tester) async {
-      final GoRouter router = await createGoRouter(tester)
-        ..push('/error');
-      router.routerDelegate.addListener(expectAsync0(() {}));
-      router.pop();
-      await tester.pump();
-    });
-
     group('canPop', () {
       testWidgets(
         'It should return false if Navigator.canPop() returns false.',
@@ -2736,7 +2727,55 @@
           expect(router.canPop(), true);
         },
       );
+      testWidgets('Pageless route should include in can pop',
+          (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> root =
+            GlobalKey<NavigatorState>(debugLabel: 'root');
+        final GlobalKey<NavigatorState> shell =
+            GlobalKey<NavigatorState>(debugLabel: 'shell');
+
+        final GoRouter router = GoRouter(
+          navigatorKey: root,
+          routes: <RouteBase>[
+            ShellRoute(
+              navigatorKey: shell,
+              builder:
+                  (BuildContext context, GoRouterState state, Widget child) {
+                return Scaffold(
+                  body: Center(
+                    child: Column(
+                      children: <Widget>[
+                        const Text('Shell'),
+                        Expanded(child: child),
+                      ],
+                    ),
+                  ),
+                );
+              },
+              routes: <RouteBase>[
+                GoRoute(
+                  path: '/',
+                  builder: (_, __) => const Text('A Screen'),
+                ),
+              ],
+            ),
+          ],
+        );
+
+        await tester.pumpWidget(MaterialApp.router(routerConfig: router));
+
+        expect(router.canPop(), isFalse);
+        expect(find.text('A Screen'), findsOneWidget);
+        expect(find.text('Shell'), findsOneWidget);
+        showDialog(
+            context: root.currentContext!,
+            builder: (_) => const Text('A dialog'));
+        await tester.pumpAndSettle();
+        expect(find.text('A dialog'), findsOneWidget);
+        expect(router.canPop(), isTrue);
+      });
     });
+
     group('pop', () {
       testWidgets(
         'Should pop from the correct navigator when parentNavigatorKey is set',
@@ -2815,6 +2854,74 @@
           expect(find.text('Shell'), findsNothing);
         },
       );
+
+      testWidgets('Should pop dialog if it is present',
+          (WidgetTester tester) async {
+        final GlobalKey<NavigatorState> root =
+            GlobalKey<NavigatorState>(debugLabel: 'root');
+        final GlobalKey<NavigatorState> shell =
+            GlobalKey<NavigatorState>(debugLabel: 'shell');
+
+        final GoRouter router = GoRouter(
+          initialLocation: '/a',
+          navigatorKey: root,
+          routes: <GoRoute>[
+            GoRoute(
+              path: '/',
+              builder: (BuildContext context, _) {
+                return const Scaffold(
+                  body: Text('Home'),
+                );
+              },
+              routes: <RouteBase>[
+                ShellRoute(
+                  navigatorKey: shell,
+                  builder: (BuildContext context, GoRouterState state,
+                      Widget child) {
+                    return Scaffold(
+                      body: Center(
+                        child: Column(
+                          children: <Widget>[
+                            const Text('Shell'),
+                            Expanded(child: child),
+                          ],
+                        ),
+                      ),
+                    );
+                  },
+                  routes: <RouteBase>[
+                    GoRoute(
+                      path: 'a',
+                      builder: (_, __) => const Text('A Screen'),
+                    ),
+                  ],
+                ),
+              ],
+            ),
+          ],
+        );
+
+        await tester.pumpWidget(MaterialApp.router(routerConfig: router));
+
+        expect(router.canPop(), isTrue);
+        expect(find.text('A Screen'), findsOneWidget);
+        expect(find.text('Shell'), findsOneWidget);
+        expect(find.text('Home'), findsNothing);
+        final Future<bool?> resultFuture = showDialog<bool>(
+            context: root.currentContext!,
+            builder: (_) => const Text('A dialog'));
+        await tester.pumpAndSettle();
+        expect(find.text('A dialog'), findsOneWidget);
+        expect(router.canPop(), isTrue);
+
+        router.pop<bool>(true);
+        await tester.pumpAndSettle();
+        expect(find.text('A Screen'), findsOneWidget);
+        expect(find.text('Shell'), findsOneWidget);
+        expect(find.text('A dialog'), findsNothing);
+        final bool? result = await resultFuture;
+        expect(result, isTrue);
+      });
     });
   });
 }
diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart
index be35707..9ead9a6 100644
--- a/packages/go_router/test/test_helpers.dart
+++ b/packages/go_router/test/test_helpers.dart
@@ -130,7 +130,7 @@
   bool popped = false;
 
   @override
-  void pop() {
+  void pop<T extends Object?>([T? result]) {
     popped = true;
   }
 }