[go_router] Adds parent navigator key to ShellRoute and StatefulShell… (#4201)

…Route.

fixes https://github.com/flutter/flutter/issues/111678
fixes https://github.com/flutter/flutter/issues/128793
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index b0df45c..b530a9a 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 8.1.0
+
+- Adds parent navigator key to ShellRoute and StatefulShellRoute.
+
 ## 8.0.5
 
 - Fixes a bug that GoRouterState in top level redirect doesn't contain complete data.
diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart
index c98f2f8..26d70ae 100644
--- a/packages/go_router/lib/src/builder.dart
+++ b/packages/go_router/lib/src/builder.dart
@@ -204,77 +204,73 @@
       keyToPages.putIfAbsent(navigatorKey, () => <Page<Object?>>[]).add(page);
       _buildRecursive(context, matchList, startIndex + 1, pagePopContext,
           routerNeglect, keyToPages, navigatorKey, registry);
-    } else if (route is GoRoute) {
-      page = _buildPageForGoRoute(context, state, match, route, pagePopContext);
-      // If this GoRoute is for a different Navigator, add it to the
+    } else {
+      // If this RouteBase is for a different Navigator, add it to the
       // list of out of scope pages
-      final GlobalKey<NavigatorState> goRouteNavKey =
+      final GlobalKey<NavigatorState> routeNavKey =
           route.parentNavigatorKey ?? navigatorKey;
+      if (route is GoRoute) {
+        page =
+            _buildPageForGoRoute(context, state, match, route, pagePopContext);
 
-      keyToPages.putIfAbsent(goRouteNavKey, () => <Page<Object?>>[]).add(page);
+        keyToPages.putIfAbsent(routeNavKey, () => <Page<Object?>>[]).add(page);
 
-      _buildRecursive(context, matchList, startIndex + 1, pagePopContext,
-          routerNeglect, keyToPages, navigatorKey, registry);
-    } else if (route is ShellRouteBase) {
-      assert(startIndex + 1 < matchList.matches.length,
-          'Shell routes must always have child routes');
-      // The key for the Navigator that will display this ShellRoute's page.
-      final GlobalKey<NavigatorState> parentNavigatorKey = navigatorKey;
+        _buildRecursive(context, matchList, startIndex + 1, pagePopContext,
+            routerNeglect, keyToPages, navigatorKey, registry);
+      } else if (route is ShellRouteBase) {
+        assert(startIndex + 1 < matchList.matches.length,
+            'Shell routes must always have child routes');
 
-      // Add an entry for the parent navigator if none exists.
-      keyToPages.putIfAbsent(parentNavigatorKey, () => <Page<Object?>>[]);
+        // Add an entry for the parent navigator if none exists.
+        //
+        // Calling _buildRecursive can result in adding pages to the
+        // parentNavigatorKey entry's list. Store the current length so
+        // that the page for this ShellRoute is placed at the right index.
+        final int shellPageIdx =
+            keyToPages.putIfAbsent(routeNavKey, () => <Page<Object?>>[]).length;
 
-      // Calling _buildRecursive can result in adding pages to the
-      // parentNavigatorKey entry's list. Store the current length so
-      // that the page for this ShellRoute is placed at the right index.
-      final int shellPageIdx = keyToPages[parentNavigatorKey]!.length;
+        // Find the the navigator key for the sub-route of this shell route.
+        final RouteBase subRoute = matchList.matches[startIndex + 1].route;
+        final GlobalKey<NavigatorState> shellNavigatorKey =
+            route.navigatorKeyForSubRoute(subRoute);
 
-      // Get the current sub-route of this shell route from the match list.
-      final RouteBase subRoute = matchList.matches[startIndex + 1].route;
+        keyToPages.putIfAbsent(shellNavigatorKey, () => <Page<Object?>>[]);
 
-      // The key to provide to the shell route's Navigator.
-      final GlobalKey<NavigatorState> shellNavigatorKey =
-          route.navigatorKeyForSubRoute(subRoute);
+        // Build the remaining pages
+        _buildRecursive(context, matchList, startIndex + 1, pagePopContext,
+            routerNeglect, keyToPages, shellNavigatorKey, registry);
 
-      // Add an entry for the shell route's navigator
-      keyToPages.putIfAbsent(shellNavigatorKey, () => <Page<Object?>>[]);
+        final HeroController heroController = _goHeroCache.putIfAbsent(
+            shellNavigatorKey, () => _getHeroController(context));
 
-      // Build the remaining pages
-      _buildRecursive(context, matchList, startIndex + 1, pagePopContext,
-          routerNeglect, keyToPages, shellNavigatorKey, registry);
+        // Build the Navigator for this shell route
+        Widget buildShellNavigator(
+            List<NavigatorObserver>? observers, String? restorationScopeId) {
+          return _buildNavigator(
+            pagePopContext.onPopPage,
+            keyToPages[shellNavigatorKey]!,
+            shellNavigatorKey,
+            observers: observers ?? const <NavigatorObserver>[],
+            restorationScopeId: restorationScopeId,
+            heroController: heroController,
+          );
+        }
 
-      final HeroController heroController = _goHeroCache.putIfAbsent(
-          shellNavigatorKey, () => _getHeroController(context));
-
-      // Build the Navigator for this shell route
-      Widget buildShellNavigator(
-          List<NavigatorObserver>? observers, String? restorationScopeId) {
-        return _buildNavigator(
-          pagePopContext.onPopPage,
-          keyToPages[shellNavigatorKey]!,
-          shellNavigatorKey,
-          observers: observers ?? const <NavigatorObserver>[],
-          restorationScopeId: restorationScopeId,
-          heroController: heroController,
+        // Call the ShellRouteBase to create/update the shell route state
+        final ShellRouteContext shellRouteContext = ShellRouteContext(
+          route: route,
+          routerState: state,
+          navigatorKey: shellNavigatorKey,
+          routeMatchList: matchList,
+          navigatorBuilder: buildShellNavigator,
         );
+
+        // Build the Page for this route
+        page = _buildPageForShellRoute(
+            context, state, match, route, pagePopContext, shellRouteContext);
+        // Place the ShellRoute's Page onto the list for the parent navigator.
+        keyToPages[routeNavKey]!.insert(shellPageIdx, page);
       }
-
-      // Call the ShellRouteBase to create/update the shell route state
-      final ShellRouteContext shellRouteContext = ShellRouteContext(
-        route: route,
-        routerState: state,
-        navigatorKey: shellNavigatorKey,
-        routeMatchList: matchList,
-        navigatorBuilder: buildShellNavigator,
-      );
-
-      // Build the Page for this route
-      page = _buildPageForShellRoute(
-          context, state, match, route, pagePopContext, shellRouteContext);
-      // Place the ShellRoute's Page onto the list for the parent navigator.
-      keyToPages
-          .putIfAbsent(parentNavigatorKey, () => <Page<Object?>>[])
-          .insert(shellPageIdx, page);
     }
     if (page != null) {
       registry[page] = state;
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 2ba3729..3798671 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -148,73 +148,61 @@
 /// pageless route, such as a dialog or bottom sheet.
 class _NavigatorStateIterator extends Iterator<NavigatorState> {
   _NavigatorStateIterator(this.matchList, this.root)
-      : index = matchList.matches.length;
+      : index = matchList.matches.length - 1;
 
   final RouteMatchList matchList;
-  int index = 0;
+  int index;
+
   final NavigatorState root;
   @override
   late NavigatorState current;
 
+  RouteBase _getRouteAtIndex(int index) => matchList.matches[index].route;
+
+  void _findsNextIndex() {
+    final GlobalKey<NavigatorState>? parentNavigatorKey =
+        _getRouteAtIndex(index).parentNavigatorKey;
+    if (parentNavigatorKey == null) {
+      index -= 1;
+      return;
+    }
+
+    for (index -= 1; index >= 0; index -= 1) {
+      final RouteBase route = _getRouteAtIndex(index);
+      if (route is ShellRouteBase) {
+        if (route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1)) ==
+            parentNavigatorKey) {
+          return;
+        }
+      }
+    }
+    assert(root == parentNavigatorKey.currentState);
+  }
+
   @override
   bool moveNext() {
     if (index < 0) {
       return false;
     }
-    late RouteBase subRoute;
-    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;
-        }
+    _findsNextIndex();
 
-        current = parentNavigatorKey.currentState!;
-        return true;
-      } else if (route is ShellRouteBase) {
+    while (index >= 0) {
+      final RouteBase route = _getRouteAtIndex(index);
+      if (route is ShellRouteBase) {
+        final GlobalKey<NavigatorState> navigatorKey =
+            route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1));
         // Must have a ModalRoute parent because the navigator ShellRoute
         // created must not be the root navigator.
-        final GlobalKey<NavigatorState> navigatorKey =
-            route.navigatorKeyForSubRoute(subRoute);
         final ModalRoute<Object?> parentModalRoute =
             ModalRoute.of(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;
+        if (parentModalRoute.isCurrent) {
+          current = navigatorKey.currentState!;
+          return true;
         }
-        current = navigatorKey.currentState!;
-        return true;
       }
-      subRoute = route;
+      _findsNextIndex();
     }
     assert(index == -1);
     current = root;
diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart
index ba65632..9d33e88 100644
--- a/packages/go_router/lib/src/route.dart
+++ b/packages/go_router/lib/src/route.dart
@@ -98,12 +98,20 @@
 @immutable
 abstract class RouteBase {
   const RouteBase._({
-    this.routes = const <RouteBase>[],
+    required this.routes,
+    required this.parentNavigatorKey,
   });
 
   /// The list of child routes associated with this route.
   final List<RouteBase> routes;
 
+  /// An optional key specifying which Navigator to display this route's screen
+  /// onto.
+  ///
+  /// Specifying the root Navigator will stack this route onto that
+  /// Navigator instead of the nearest ShellRoute ancestor.
+  final GlobalKey<NavigatorState>? parentNavigatorKey;
+
   /// Builds a lists containing the provided routes along with all their
   /// descendant [routes].
   static Iterable<RouteBase> routesRecursively(Iterable<RouteBase> routes) {
@@ -137,7 +145,7 @@
     this.name,
     this.builder,
     this.pageBuilder,
-    this.parentNavigatorKey,
+    super.parentNavigatorKey,
     this.redirect,
     super.routes = const <RouteBase>[],
   })  : assert(path.isNotEmpty, 'GoRoute path cannot be empty'),
@@ -301,13 +309,6 @@
   /// re-evaluation will be triggered if the [InheritedWidget] changes.
   final GoRouterRedirect? redirect;
 
-  /// An optional key specifying which Navigator to display this route's screen
-  /// onto.
-  ///
-  /// Specifying the root Navigator will stack this route onto that
-  /// Navigator instead of the nearest ShellRoute ancestor.
-  final GlobalKey<NavigatorState>? parentNavigatorKey;
-
   // TODO(chunhtai): move all regex related help methods to path_utils.dart.
   /// Match this route against a location.
   RegExpMatch? matchPatternAsPrefix(String loc) =>
@@ -333,7 +334,9 @@
 /// as [ShellRoute] and [StatefulShellRoute].
 abstract class ShellRouteBase extends RouteBase {
   /// Constructs a [ShellRouteBase].
-  const ShellRouteBase._({super.routes}) : super._();
+  const ShellRouteBase._(
+      {required super.routes, required super.parentNavigatorKey})
+      : super._();
 
   /// Attempts to build the Widget representing this shell route.
   ///
@@ -496,7 +499,8 @@
     this.builder,
     this.pageBuilder,
     this.observers,
-    super.routes,
+    required super.routes,
+    super.parentNavigatorKey,
     GlobalKey<NavigatorState>? navigatorKey,
     this.restorationScopeId,
   })  : assert(routes.isNotEmpty),
@@ -653,6 +657,7 @@
     this.builder,
     this.pageBuilder,
     required this.navigatorContainerBuilder,
+    super.parentNavigatorKey,
     this.restorationScopeId,
   })  : assert(branches.isNotEmpty),
         assert((pageBuilder != null) ^ (builder != null),
@@ -676,12 +681,14 @@
   StatefulShellRoute.indexedStack({
     required List<StatefulShellBranch> branches,
     StatefulShellRouteBuilder? builder,
+    GlobalKey<NavigatorState>? parentNavigatorKey,
     StatefulShellRoutePageBuilder? pageBuilder,
     String? restorationScopeId,
   }) : this(
           branches: branches,
           builder: builder,
           pageBuilder: pageBuilder,
+          parentNavigatorKey: parentNavigatorKey,
           restorationScopeId: restorationScopeId,
           navigatorContainerBuilder: _indexedStackContainerBuilder,
         );
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index db489ed..ded370a 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: 8.0.5
+version: 8.1.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 859a4f2..1b1d5e5 100644
--- a/packages/go_router/test/delegate_test.dart
+++ b/packages/go_router/test/delegate_test.dart
@@ -7,6 +7,7 @@
 import 'package:go_router/go_router.dart';
 import 'package:go_router/src/match.dart';
 import 'package:go_router/src/misc/error_screen.dart';
+import 'package:go_router/src/misc/errors.dart';
 
 import 'test_helpers.dart';
 
@@ -96,6 +97,73 @@
       await goRouter.routerDelegate.popRoute();
       expect(await goRouter.routerDelegate.popRoute(), isFalse);
     });
+
+    testWidgets('throw if nothing to pop', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootKey = GlobalKey<NavigatorState>();
+      final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
+      final GoRouter goRouter = await createRouter(
+        <RouteBase>[
+          ShellRoute(
+            navigatorKey: rootKey,
+            builder: (_, __, Widget child) => child,
+            routes: <RouteBase>[
+              ShellRoute(
+                parentNavigatorKey: rootKey,
+                navigatorKey: navKey,
+                builder: (_, __, Widget child) => child,
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: '/',
+                    parentNavigatorKey: navKey,
+                    builder: (_, __) => const Text('Home'),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ],
+        tester,
+      );
+      await tester.pumpAndSettle();
+      expect(find.text('Home'), findsOneWidget);
+      String? message;
+      try {
+        goRouter.pop();
+      } on GoError catch (e) {
+        message = e.message;
+      }
+      expect(message, 'There is nothing to pop');
+    });
+
+    testWidgets('poproute return false if nothing to pop',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootKey = GlobalKey<NavigatorState>();
+      final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
+      final GoRouter goRouter = await createRouter(
+        <RouteBase>[
+          ShellRoute(
+            navigatorKey: rootKey,
+            builder: (_, __, Widget child) => child,
+            routes: <RouteBase>[
+              ShellRoute(
+                parentNavigatorKey: rootKey,
+                navigatorKey: navKey,
+                builder: (_, __, Widget child) => child,
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: '/',
+                    parentNavigatorKey: navKey,
+                    builder: (_, __) => const Text('Home'),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ],
+        tester,
+      );
+      expect(await goRouter.routerDelegate.popRoute(), isFalse);
+    });
   });
 
   group('push', () {
diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart
index 31361aa..f7ffe70 100644
--- a/packages/go_router/test/go_route_test.dart
+++ b/packages/go_router/test/go_route_test.dart
@@ -2,9 +2,12 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/go_router.dart';
 
+import 'test_helpers.dart';
+
 void main() {
   test('throws when a builder is not set', () {
     expect(() => GoRoute(path: '/'), throwsA(isAssertionError));
@@ -17,4 +20,139 @@
   test('does not throw when only redirect is provided', () {
     GoRoute(path: '/', redirect: (_, __) => '/a');
   });
+
+  testWidgets('ShellRoute can use parent navigator key',
+      (WidgetTester tester) async {
+    final GlobalKey<NavigatorState> rootNavigatorKey =
+        GlobalKey<NavigatorState>();
+    final GlobalKey<NavigatorState> shellNavigatorKey =
+        GlobalKey<NavigatorState>();
+
+    final List<RouteBase> routes = <RouteBase>[
+      ShellRoute(
+        navigatorKey: shellNavigatorKey,
+        builder: (BuildContext context, GoRouterState state, Widget child) {
+          return Scaffold(
+            body: Column(
+              children: <Widget>[
+                const Text('Screen A'),
+                Expanded(child: child),
+              ],
+            ),
+          );
+        },
+        routes: <RouteBase>[
+          GoRoute(
+            path: '/b',
+            builder: (BuildContext context, GoRouterState state) {
+              return const Scaffold(
+                body: Text('Screen B'),
+              );
+            },
+            routes: <RouteBase>[
+              ShellRoute(
+                parentNavigatorKey: rootNavigatorKey,
+                builder:
+                    (BuildContext context, GoRouterState state, Widget child) {
+                  return Scaffold(
+                    body: Column(
+                      children: <Widget>[
+                        const Text('Screen D'),
+                        Expanded(child: child),
+                      ],
+                    ),
+                  );
+                },
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: 'c',
+                    builder: (BuildContext context, GoRouterState state) {
+                      return const Scaffold(
+                        body: Text('Screen C'),
+                      );
+                    },
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ],
+      ),
+    ];
+
+    await createRouter(routes, tester,
+        initialLocation: '/b/c', navigatorKey: rootNavigatorKey);
+    expect(find.text('Screen A'), findsNothing);
+    expect(find.text('Screen B'), findsNothing);
+    expect(find.text('Screen D'), findsOneWidget);
+    expect(find.text('Screen C'), findsOneWidget);
+  });
+
+  testWidgets('StatefulShellRoute can use parent navigator key',
+      (WidgetTester tester) async {
+    final GlobalKey<NavigatorState> rootNavigatorKey =
+        GlobalKey<NavigatorState>();
+    final GlobalKey<NavigatorState> shellNavigatorKey =
+        GlobalKey<NavigatorState>();
+
+    final List<RouteBase> routes = <RouteBase>[
+      ShellRoute(
+        navigatorKey: shellNavigatorKey,
+        builder: (BuildContext context, GoRouterState state, Widget child) {
+          return Scaffold(
+            body: Column(
+              children: <Widget>[
+                const Text('Screen A'),
+                Expanded(child: child),
+              ],
+            ),
+          );
+        },
+        routes: <RouteBase>[
+          GoRoute(
+            path: '/b',
+            builder: (BuildContext context, GoRouterState state) {
+              return const Scaffold(
+                body: Text('Screen B'),
+              );
+            },
+            routes: <RouteBase>[
+              StatefulShellRoute.indexedStack(
+                parentNavigatorKey: rootNavigatorKey,
+                builder: (_, __, StatefulNavigationShell navigationShell) {
+                  return Column(
+                    children: <Widget>[
+                      const Text('Screen D'),
+                      Expanded(child: navigationShell),
+                    ],
+                  );
+                },
+                branches: <StatefulShellBranch>[
+                  StatefulShellBranch(
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'c',
+                        builder: (BuildContext context, GoRouterState state) {
+                          return const Scaffold(
+                            body: Text('Screen C'),
+                          );
+                        },
+                      ),
+                    ],
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ],
+      ),
+    ];
+
+    await createRouter(routes, tester,
+        initialLocation: '/b/c', navigatorKey: rootNavigatorKey);
+    expect(find.text('Screen A'), findsNothing);
+    expect(find.text('Screen B'), findsNothing);
+    expect(find.text('Screen D'), findsOneWidget);
+    expect(find.text('Screen C'), findsOneWidget);
+  });
 }