[go_router] Nested stateful navigation with ShellRoute (#2650)

Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 43869ef..dc2b5c0 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 7.1.0
+
+- Introduces `StatefulShellRoute` to support using separate navigators for child routes as well as preserving state in each navigation tree (flutter/flutter#99124).
+- Updates documentation for `pageBuilder` and `builder` fields of `ShellRoute`, to more correctly
+  describe the meaning of the child argument in the builder functions.
+- Adds support for restorationId to ShellRoute (and StatefulShellRoute).
+
 ## 7.0.2
 
 - Fixes `BuildContext` extension method `replaceNamed` to correctly pass `pathParameters` and `queryParameters`.
diff --git a/packages/go_router/example/README.md b/packages/go_router/example/README.md
index 90c7aad..f4c1c0f 100644
--- a/packages/go_router/example/README.md
+++ b/packages/go_router/example/README.md
@@ -30,6 +30,12 @@
 
 An example to demonstrate how to use handle a sign-in flow with a stream authentication service.
 
+## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart)
+`flutter run lib/stacked_shell_route.dart`
+
+An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a
+`BottomNavigationBar`.
+
 ## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books)
 `flutter run lib/books/main.dart`
 
diff --git a/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard b/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard
index f3c2851..bb61264 100644
--- a/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard
+++ b/packages/go_router/example/ios/Runner/Base.lproj/Main.storyboard
@@ -8,7 +8,7 @@
         <!--Flutter View Controller-->
         <scene sceneID="tne-QT-ifu">
             <objects>
-                <viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
+                <viewController restorationIdentifier="sampleRestorationId" id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
                     <layoutGuides>
                         <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
                         <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
diff --git a/packages/go_router/example/lib/others/custom_stateful_shell_route.dart b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart
new file mode 100644
index 0000000..5fbe2b6
--- /dev/null
+++ b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart
@@ -0,0 +1,460 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+
+final GlobalKey<NavigatorState> _rootNavigatorKey =
+    GlobalKey<NavigatorState>(debugLabel: 'root');
+final GlobalKey<NavigatorState> _tabANavigatorKey =
+    GlobalKey<NavigatorState>(debugLabel: 'tabANav');
+
+// This example demonstrates how to setup nested navigation using a
+// BottomNavigationBar, where each bar item uses its own persistent navigator,
+// i.e. navigation state is maintained separately for each item. This setup also
+// enables deep linking into nested pages.
+//
+// This example also demonstrates how build a nested shell with a custom
+// container for the branch Navigators (in this case a TabBarView).
+
+void main() {
+  runApp(NestedTabNavigationExampleApp());
+}
+
+/// An example demonstrating how to use nested navigators
+class NestedTabNavigationExampleApp extends StatelessWidget {
+  /// Creates a NestedTabNavigationExampleApp
+  NestedTabNavigationExampleApp({super.key});
+
+  final GoRouter _router = GoRouter(
+    navigatorKey: _rootNavigatorKey,
+    initialLocation: '/a',
+    routes: <RouteBase>[
+      StatefulShellRoute(
+        builder: (BuildContext context, GoRouterState state,
+            StatefulNavigationShell navigationShell) {
+          // This nested StatefulShellRoute demonstrates the use of a
+          // custom container for the branch Navigators. In this implementation,
+          // no customization is done in the builder function (navigationShell
+          // itself is simply used as the Widget for the route). Instead, the
+          // navigatorContainerBuilder function below is provided to
+          // customize the container for the branch Navigators.
+          return navigationShell;
+        },
+        navigatorContainerBuilder: (BuildContext context,
+            StatefulNavigationShell navigationShell, List<Widget> children) {
+          // Returning a customized container for the branch
+          // Navigators (i.e. the `List<Widget> children` argument).
+          //
+          // See ScaffoldWithNavBar for more details on how the children
+          // are managed (using AnimatedBranchContainer).
+          return ScaffoldWithNavBar(
+              navigationShell: navigationShell, children: children);
+        },
+        branches: <StatefulShellBranch>[
+          // The route branch for the first tab of the bottom navigation bar.
+          StatefulShellBranch(
+            navigatorKey: _tabANavigatorKey,
+            routes: <RouteBase>[
+              GoRoute(
+                // The screen to display as the root in the first tab of the
+                // bottom navigation bar.
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const RootScreenA(),
+                routes: <RouteBase>[
+                  // The details screen to display stacked on navigator of the
+                  // first tab. This will cover screen A but not the application
+                  // shell (bottom navigation bar).
+                  GoRoute(
+                    path: 'details',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const DetailsScreen(label: 'A'),
+                  ),
+                ],
+              ),
+            ],
+          ),
+
+          // The route branch for the third tab of the bottom navigation bar.
+          StatefulShellBranch(
+            // StatefulShellBranch will automatically use the first descendant
+            // GoRoute as the initial location of the branch. If another route
+            // is desired, specify the location of it using the defaultLocation
+            // parameter.
+            // defaultLocation: '/c2',
+            routes: <RouteBase>[
+              StatefulShellRoute(
+                builder: (BuildContext context, GoRouterState state,
+                    StatefulNavigationShell navigationShell) {
+                  // Just like with the top level StatefulShellRoute, no
+                  // customization is done in the builder function.
+                  return navigationShell;
+                },
+                navigatorContainerBuilder: (BuildContext context,
+                    StatefulNavigationShell navigationShell,
+                    List<Widget> children) {
+                  // Returning a customized container for the branch
+                  // Navigators (i.e. the `List<Widget> children` argument).
+                  //
+                  // See TabbedRootScreen for more details on how the children
+                  // are managed (in a TabBarView).
+                  return TabbedRootScreen(
+                      navigationShell: navigationShell, children: children);
+                },
+                // This bottom tab uses a nested shell, wrapping sub routes in a
+                // top TabBar.
+                branches: <StatefulShellBranch>[
+                  StatefulShellBranch(routes: <GoRoute>[
+                    GoRoute(
+                      path: '/b1',
+                      builder: (BuildContext context, GoRouterState state) =>
+                          const TabScreen(
+                              label: 'B1', detailsPath: '/b1/details'),
+                      routes: <RouteBase>[
+                        GoRoute(
+                          path: 'details',
+                          builder:
+                              (BuildContext context, GoRouterState state) =>
+                                  const DetailsScreen(
+                            label: 'B1',
+                            withScaffold: false,
+                          ),
+                        ),
+                      ],
+                    ),
+                  ]),
+                  StatefulShellBranch(routes: <GoRoute>[
+                    GoRoute(
+                      path: '/b2',
+                      builder: (BuildContext context, GoRouterState state) =>
+                          const TabScreen(
+                              label: 'B2', detailsPath: '/b2/details'),
+                      routes: <RouteBase>[
+                        GoRoute(
+                          path: 'details',
+                          builder:
+                              (BuildContext context, GoRouterState state) =>
+                                  const DetailsScreen(
+                            label: 'B2',
+                            withScaffold: false,
+                          ),
+                        ),
+                      ],
+                    ),
+                  ]),
+                ],
+              ),
+            ],
+          ),
+        ],
+      ),
+    ],
+  );
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp.router(
+      title: 'Flutter Demo',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+      ),
+      routerConfig: _router,
+    );
+  }
+}
+
+/// Builds the "shell" for the app by building a Scaffold with a
+/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
+class ScaffoldWithNavBar extends StatelessWidget {
+  /// Constructs an [ScaffoldWithNavBar].
+  const ScaffoldWithNavBar({
+    required this.navigationShell,
+    required this.children,
+    Key? key,
+  }) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
+
+  /// The navigation shell and container for the branch Navigators.
+  final StatefulNavigationShell navigationShell;
+
+  /// The children (branch Navigators) to display in a custom container
+  /// ([AnimatedBranchContainer]).
+  final List<Widget> children;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: AnimatedBranchContainer(
+        currentIndex: navigationShell.currentIndex,
+        children: children,
+      ),
+      bottomNavigationBar: BottomNavigationBar(
+        // Here, the items of BottomNavigationBar are hard coded. In a real
+        // world scenario, the items would most likely be generated from the
+        // branches of the shell route, which can be fetched using
+        // `navigationShell.route.branches`.
+        items: const <BottomNavigationBarItem>[
+          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
+          BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
+        ],
+        currentIndex: navigationShell.currentIndex,
+        onTap: (int index) => _onTap(context, index),
+      ),
+    );
+  }
+
+  /// Navigate to the current location of the branch at the provided index when
+  /// tapping an item in the BottomNavigationBar.
+  void _onTap(BuildContext context, int index) {
+    // When navigating to a new branch, it's recommended to use the goBranch
+    // method, as doing so makes sure the last navigation state of the
+    // Navigator for the branch is restored.
+    navigationShell.goBranch(
+      index,
+      // A common pattern when using bottom navigation bars is to support
+      // navigating to the initial location when tapping the item that is
+      // already active. This example demonstrates how to support this behavior,
+      // using the initialLocation parameter of goBranch.
+      initialLocation: index == navigationShell.currentIndex,
+    );
+  }
+}
+
+/// Custom branch Navigator container that provides animated transitions
+/// when switching branches.
+class AnimatedBranchContainer extends StatelessWidget {
+  /// Creates a AnimatedBranchContainer
+  const AnimatedBranchContainer(
+      {super.key, required this.currentIndex, required this.children});
+
+  /// The index (in [children]) of the branch Navigator to display.
+  final int currentIndex;
+
+  /// The children (branch Navigators) to display in this container.
+  final List<Widget> children;
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+        children: children.mapIndexed(
+      (int index, Widget navigator) {
+        return AnimatedScale(
+          scale: index == currentIndex ? 1 : 1.5,
+          duration: const Duration(milliseconds: 400),
+          child: AnimatedOpacity(
+            opacity: index == currentIndex ? 1 : 0,
+            duration: const Duration(milliseconds: 400),
+            child: _branchNavigatorWrapper(index, navigator),
+          ),
+        );
+      },
+    ).toList());
+  }
+
+  Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer(
+        ignoring: index != currentIndex,
+        child: TickerMode(
+          enabled: index == currentIndex,
+          child: navigator,
+        ),
+      );
+}
+
+/// Widget for the root page for the first section of the bottom navigation bar.
+class RootScreenA extends StatelessWidget {
+  /// Creates a RootScreenA
+  const RootScreenA({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Root of section A'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: <Widget>[
+            Text('Screen A', style: Theme.of(context).textTheme.titleLarge),
+            const Padding(padding: EdgeInsets.all(4)),
+            TextButton(
+              onPressed: () {
+                GoRouter.of(context).go('/a/details');
+              },
+              child: const Text('View details'),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// The details screen for either the A or B screen.
+class DetailsScreen extends StatefulWidget {
+  /// Constructs a [DetailsScreen].
+  const DetailsScreen({
+    required this.label,
+    this.param,
+    this.withScaffold = true,
+    super.key,
+  });
+
+  /// The label to display in the center of the screen.
+  final String label;
+
+  /// Optional param
+  final String? param;
+
+  /// Wrap in scaffold
+  final bool withScaffold;
+
+  @override
+  State<StatefulWidget> createState() => DetailsScreenState();
+}
+
+/// The state for DetailsScreen
+class DetailsScreenState extends State<DetailsScreen> {
+  int _counter = 0;
+
+  @override
+  Widget build(BuildContext context) {
+    if (widget.withScaffold) {
+      return Scaffold(
+        appBar: AppBar(
+          title: Text('Details Screen - ${widget.label}'),
+        ),
+        body: _build(context),
+      );
+    } else {
+      return Container(
+        color: Theme.of(context).scaffoldBackgroundColor,
+        child: _build(context),
+      );
+    }
+  }
+
+  Widget _build(BuildContext context) {
+    return Center(
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Text('Details for ${widget.label} - Counter: $_counter',
+              style: Theme.of(context).textTheme.titleLarge),
+          const Padding(padding: EdgeInsets.all(4)),
+          TextButton(
+            onPressed: () {
+              setState(() {
+                _counter++;
+              });
+            },
+            child: const Text('Increment counter'),
+          ),
+          const Padding(padding: EdgeInsets.all(8)),
+          if (widget.param != null)
+            Text('Parameter: ${widget.param!}',
+                style: Theme.of(context).textTheme.titleMedium),
+          const Padding(padding: EdgeInsets.all(8)),
+          if (!widget.withScaffold) ...<Widget>[
+            const Padding(padding: EdgeInsets.all(16)),
+            TextButton(
+              onPressed: () {
+                GoRouter.of(context).pop();
+              },
+              child: const Text('< Back',
+                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
+            ),
+          ]
+        ],
+      ),
+    );
+  }
+}
+
+/// Builds a nested shell using a [TabBar] and [TabBarView].
+class TabbedRootScreen extends StatefulWidget {
+  /// Constructs a TabbedRootScreen
+  const TabbedRootScreen(
+      {required this.navigationShell, required this.children, super.key});
+
+  /// The current state of the parent StatefulShellRoute.
+  final StatefulNavigationShell navigationShell;
+
+  /// The children (branch Navigators) to display in the [TabBarView].
+  final List<Widget> children;
+
+  @override
+  State<StatefulWidget> createState() => _TabbedRootScreenState();
+}
+
+class _TabbedRootScreenState extends State<TabbedRootScreen>
+    with SingleTickerProviderStateMixin {
+  late final TabController _tabController = TabController(
+      length: widget.children.length,
+      vsync: this,
+      initialIndex: widget.navigationShell.currentIndex);
+
+  @override
+  void didUpdateWidget(covariant TabbedRootScreen oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    _tabController.index = widget.navigationShell.currentIndex;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Tab> tabs = widget.children
+        .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}'))
+        .toList();
+
+    return Scaffold(
+      appBar: AppBar(
+          title: const Text('Root of Section B (nested TabBar shell)'),
+          bottom: TabBar(
+            controller: _tabController,
+            tabs: tabs,
+            onTap: (int tappedIndex) => _onTabTap(context, tappedIndex),
+          )),
+      body: TabBarView(
+        controller: _tabController,
+        children: widget.children,
+      ),
+    );
+  }
+
+  void _onTabTap(BuildContext context, int index) {
+    widget.navigationShell.goBranch(index);
+  }
+}
+
+/// Widget for the pages in the top tab bar.
+class TabScreen extends StatelessWidget {
+  /// Creates a RootScreen
+  const TabScreen({required this.label, required this.detailsPath, super.key});
+
+  /// The label
+  final String label;
+
+  /// The path to the detail page
+  final String detailsPath;
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Text('Screen $label', style: Theme.of(context).textTheme.titleLarge),
+          const Padding(padding: EdgeInsets.all(4)),
+          TextButton(
+            onPressed: () {
+              GoRouter.of(context).go(detailsPath);
+            },
+            child: const Text('View details'),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart b/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart
new file mode 100644
index 0000000..aeecd11
--- /dev/null
+++ b/packages/go_router/example/lib/others/stateful_shell_state_restoration.dart
@@ -0,0 +1,236 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// 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:go_router/go_router.dart';
+
+void main() => runApp(RestorableStatefulShellRouteExampleApp());
+
+/// An example demonstrating how to use StatefulShellRoute with state
+/// restoration.
+class RestorableStatefulShellRouteExampleApp extends StatelessWidget {
+  /// Creates a NestedTabNavigationExampleApp
+  RestorableStatefulShellRouteExampleApp({super.key});
+
+  final GoRouter _router = GoRouter(
+    initialLocation: '/a',
+    restorationScopeId: 'router',
+    routes: <RouteBase>[
+      StatefulShellRoute.indexedStack(
+        restorationScopeId: 'shell1',
+        pageBuilder: (BuildContext context, GoRouterState state,
+            StatefulNavigationShell navigationShell) {
+          return MaterialPage<void>(
+              restorationId: 'shellWidget1',
+              child: ScaffoldWithNavBar(navigationShell: navigationShell));
+        },
+        branches: <StatefulShellBranch>[
+          // The route branch for the first tab of the bottom navigation bar.
+          StatefulShellBranch(
+            restorationScopeId: 'branchA',
+            routes: <RouteBase>[
+              GoRoute(
+                // The screen to display as the root in the first tab of the
+                // bottom navigation bar.
+                path: '/a',
+                pageBuilder: (BuildContext context, GoRouterState state) =>
+                    const MaterialPage<void>(
+                        restorationId: 'screenA',
+                        child:
+                            RootScreen(label: 'A', detailsPath: '/a/details')),
+                routes: <RouteBase>[
+                  // The details screen to display stacked on navigator of the
+                  // first tab. This will cover screen A but not the application
+                  // shell (bottom navigation bar).
+                  GoRoute(
+                    path: 'details',
+                    pageBuilder: (BuildContext context, GoRouterState state) =>
+                        const MaterialPage<void>(
+                            restorationId: 'screenADetail',
+                            child: DetailsScreen(label: 'A')),
+                  ),
+                ],
+              ),
+            ],
+          ),
+          // The route branch for the second tab of the bottom navigation bar.
+          StatefulShellBranch(
+            restorationScopeId: 'branchB',
+            routes: <RouteBase>[
+              GoRoute(
+                // The screen to display as the root in the second tab of the
+                // bottom navigation bar.
+                path: '/b',
+                pageBuilder: (BuildContext context, GoRouterState state) =>
+                    const MaterialPage<void>(
+                        restorationId: 'screenB',
+                        child:
+                            RootScreen(label: 'B', detailsPath: '/b/details')),
+                routes: <RouteBase>[
+                  // The details screen to display stacked on navigator of the
+                  // first tab. This will cover screen A but not the application
+                  // shell (bottom navigation bar).
+                  GoRoute(
+                    path: 'details',
+                    pageBuilder: (BuildContext context, GoRouterState state) =>
+                        const MaterialPage<void>(
+                            restorationId: 'screenBDetail',
+                            child: DetailsScreen(label: 'B')),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ],
+      ),
+    ],
+  );
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp.router(
+      restorationScopeId: 'app',
+      title: 'Flutter Demo',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+      ),
+      routerConfig: _router,
+    );
+  }
+}
+
+/// Builds the "shell" for the app by building a Scaffold with a
+/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
+class ScaffoldWithNavBar extends StatelessWidget {
+  /// Constructs an [ScaffoldWithNavBar].
+  const ScaffoldWithNavBar({
+    required this.navigationShell,
+    Key? key,
+  }) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
+
+  /// The navigation shell and container for the branch Navigators.
+  final StatefulNavigationShell navigationShell;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: navigationShell,
+      bottomNavigationBar: BottomNavigationBar(
+        items: const <BottomNavigationBarItem>[
+          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
+          BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
+        ],
+        currentIndex: navigationShell.currentIndex,
+        onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex),
+      ),
+    );
+  }
+}
+
+/// Widget for the root/initial pages in the bottom navigation bar.
+class RootScreen extends StatelessWidget {
+  /// Creates a RootScreen
+  const RootScreen({
+    required this.label,
+    required this.detailsPath,
+    super.key,
+  });
+
+  /// The label
+  final String label;
+
+  /// The path to the detail page
+  final String detailsPath;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Root of section $label'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: <Widget>[
+            Text('Screen $label',
+                style: Theme.of(context).textTheme.titleLarge),
+            const Padding(padding: EdgeInsets.all(4)),
+            TextButton(
+              onPressed: () {
+                GoRouter.of(context).go(detailsPath);
+              },
+              child: const Text('View details'),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// The details screen for either the A or B screen.
+class DetailsScreen extends StatefulWidget {
+  /// Constructs a [DetailsScreen].
+  const DetailsScreen({
+    required this.label,
+    super.key,
+  });
+
+  /// The label to display in the center of the screen.
+  final String label;
+
+  @override
+  State<StatefulWidget> createState() => DetailsScreenState();
+}
+
+/// The state for DetailsScreen
+class DetailsScreenState extends State<DetailsScreen> with RestorationMixin {
+  final RestorableInt _counter = RestorableInt(0);
+
+  @override
+  String? get restorationId => 'DetailsScreen-${widget.label}';
+
+  @override
+  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
+    registerForRestoration(_counter, 'counter');
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _counter.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Details Screen - ${widget.label}'),
+      ),
+      body: _build(context),
+    );
+  }
+
+  Widget _build(BuildContext context) {
+    return Center(
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Text('Details for ${widget.label} - Counter: ${_counter.value}',
+              style: Theme.of(context).textTheme.titleLarge),
+          const Padding(padding: EdgeInsets.all(4)),
+          TextButton(
+            onPressed: () {
+              setState(() {
+                _counter.value++;
+              });
+            },
+            child: const Text('Increment counter'),
+          ),
+          const Padding(padding: EdgeInsets.all(8)),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart
new file mode 100644
index 0000000..eb0a67b
--- /dev/null
+++ b/packages/go_router/example/lib/stateful_shell_route.dart
@@ -0,0 +1,324 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// 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:go_router/go_router.dart';
+
+final GlobalKey<NavigatorState> _rootNavigatorKey =
+    GlobalKey<NavigatorState>(debugLabel: 'root');
+final GlobalKey<NavigatorState> _sectionANavigatorKey =
+    GlobalKey<NavigatorState>(debugLabel: 'sectionANav');
+
+// This example demonstrates how to setup nested navigation using a
+// BottomNavigationBar, where each bar item uses its own persistent navigator,
+// i.e. navigation state is maintained separately for each item. This setup also
+// enables deep linking into nested pages.
+
+void main() {
+  runApp(NestedTabNavigationExampleApp());
+}
+
+/// An example demonstrating how to use nested navigators
+class NestedTabNavigationExampleApp extends StatelessWidget {
+  /// Creates a NestedTabNavigationExampleApp
+  NestedTabNavigationExampleApp({super.key});
+
+  final GoRouter _router = GoRouter(
+    navigatorKey: _rootNavigatorKey,
+    initialLocation: '/a',
+    routes: <RouteBase>[
+      StatefulShellRoute.indexedStack(
+        builder: (BuildContext context, GoRouterState state,
+            StatefulNavigationShell navigationShell) {
+          // Return the widget that implements the custom shell (in this case
+          // using a BottomNavigationBar). The StatefulNavigationShell is passed
+          // to be able access the state of the shell and to navigate to other
+          // branches in a stateful way.
+          return ScaffoldWithNavBar(navigationShell: navigationShell);
+        },
+        branches: <StatefulShellBranch>[
+          // The route branch for the first tab of the bottom navigation bar.
+          StatefulShellBranch(
+            navigatorKey: _sectionANavigatorKey,
+            routes: <RouteBase>[
+              GoRoute(
+                // The screen to display as the root in the first tab of the
+                // bottom navigation bar.
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const RootScreen(label: 'A', detailsPath: '/a/details'),
+                routes: <RouteBase>[
+                  // The details screen to display stacked on navigator of the
+                  // first tab. This will cover screen A but not the application
+                  // shell (bottom navigation bar).
+                  GoRoute(
+                    path: 'details',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const DetailsScreen(label: 'A'),
+                  ),
+                ],
+              ),
+            ],
+          ),
+
+          // The route branch for the second tab of the bottom navigation bar.
+          StatefulShellBranch(
+            // It's not necessary to provide a navigatorKey if it isn't also
+            // needed elsewhere. If not provided, a default key will be used.
+            routes: <RouteBase>[
+              GoRoute(
+                // The screen to display as the root in the second tab of the
+                // bottom navigation bar.
+                path: '/b',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const RootScreen(
+                  label: 'B',
+                  detailsPath: '/b/details/1',
+                  secondDetailsPath: '/b/details/2',
+                ),
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: 'details/:param',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        DetailsScreen(
+                      label: 'B',
+                      param: state.pathParameters['param'],
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          ),
+
+          // The route branch for the third tab of the bottom navigation bar.
+          StatefulShellBranch(
+            routes: <RouteBase>[
+              GoRoute(
+                // The screen to display as the root in the third tab of the
+                // bottom navigation bar.
+                path: '/c',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const RootScreen(
+                  label: 'C',
+                  detailsPath: '/c/details',
+                ),
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: 'details',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        DetailsScreen(
+                      label: 'C',
+                      extra: state.extra,
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ],
+      ),
+    ],
+  );
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp.router(
+      title: 'Flutter Demo',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+      ),
+      routerConfig: _router,
+    );
+  }
+}
+
+/// Builds the "shell" for the app by building a Scaffold with a
+/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
+class ScaffoldWithNavBar extends StatelessWidget {
+  /// Constructs an [ScaffoldWithNavBar].
+  const ScaffoldWithNavBar({
+    required this.navigationShell,
+    Key? key,
+  }) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
+
+  /// The navigation shell and container for the branch Navigators.
+  final StatefulNavigationShell navigationShell;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      body: navigationShell,
+      bottomNavigationBar: BottomNavigationBar(
+        // Here, the items of BottomNavigationBar are hard coded. In a real
+        // world scenario, the items would most likely be generated from the
+        // branches of the shell route, which can be fetched using
+        // `navigationShell.route.branches`.
+        items: const <BottomNavigationBarItem>[
+          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
+          BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
+          BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'),
+        ],
+        currentIndex: navigationShell.currentIndex,
+        onTap: (int index) => _onTap(context, index),
+      ),
+    );
+  }
+
+  /// Navigate to the current location of the branch at the provided index when
+  /// tapping an item in the BottomNavigationBar.
+  void _onTap(BuildContext context, int index) {
+    // When navigating to a new branch, it's recommended to use the goBranch
+    // method, as doing so makes sure the last navigation state of the
+    // Navigator for the branch is restored.
+    navigationShell.goBranch(
+      index,
+      // A common pattern when using bottom navigation bars is to support
+      // navigating to the initial location when tapping the item that is
+      // already active. This example demonstrates how to support this behavior,
+      // using the initialLocation parameter of goBranch.
+      initialLocation: index == navigationShell.currentIndex,
+    );
+  }
+}
+
+/// Widget for the root/initial pages in the bottom navigation bar.
+class RootScreen extends StatelessWidget {
+  /// Creates a RootScreen
+  const RootScreen({
+    required this.label,
+    required this.detailsPath,
+    this.secondDetailsPath,
+    super.key,
+  });
+
+  /// The label
+  final String label;
+
+  /// The path to the detail page
+  final String detailsPath;
+
+  /// The path to another detail page
+  final String? secondDetailsPath;
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: Text('Root of section $label'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: <Widget>[
+            Text('Screen $label',
+                style: Theme.of(context).textTheme.titleLarge),
+            const Padding(padding: EdgeInsets.all(4)),
+            TextButton(
+              onPressed: () {
+                GoRouter.of(context).go(detailsPath, extra: '$label-XYZ');
+              },
+              child: const Text('View details'),
+            ),
+            const Padding(padding: EdgeInsets.all(4)),
+            if (secondDetailsPath != null)
+              TextButton(
+                onPressed: () {
+                  GoRouter.of(context).go(secondDetailsPath!);
+                },
+                child: const Text('View more details'),
+              ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// The details screen for either the A or B screen.
+class DetailsScreen extends StatefulWidget {
+  /// Constructs a [DetailsScreen].
+  const DetailsScreen({
+    required this.label,
+    this.param,
+    this.extra,
+    this.withScaffold = true,
+    super.key,
+  });
+
+  /// The label to display in the center of the screen.
+  final String label;
+
+  /// Optional param
+  final String? param;
+
+  /// Optional extra object
+  final Object? extra;
+
+  /// Wrap in scaffold
+  final bool withScaffold;
+
+  @override
+  State<StatefulWidget> createState() => DetailsScreenState();
+}
+
+/// The state for DetailsScreen
+class DetailsScreenState extends State<DetailsScreen> {
+  int _counter = 0;
+
+  @override
+  Widget build(BuildContext context) {
+    if (widget.withScaffold) {
+      return Scaffold(
+        appBar: AppBar(
+          title: Text('Details Screen - ${widget.label}'),
+        ),
+        body: _build(context),
+      );
+    } else {
+      return Container(
+        color: Theme.of(context).scaffoldBackgroundColor,
+        child: _build(context),
+      );
+    }
+  }
+
+  Widget _build(BuildContext context) {
+    return Center(
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Text('Details for ${widget.label} - Counter: $_counter',
+              style: Theme.of(context).textTheme.titleLarge),
+          const Padding(padding: EdgeInsets.all(4)),
+          TextButton(
+            onPressed: () {
+              setState(() {
+                _counter++;
+              });
+            },
+            child: const Text('Increment counter'),
+          ),
+          const Padding(padding: EdgeInsets.all(8)),
+          if (widget.param != null)
+            Text('Parameter: ${widget.param!}',
+                style: Theme.of(context).textTheme.titleMedium),
+          const Padding(padding: EdgeInsets.all(8)),
+          if (widget.extra != null)
+            Text('Extra: ${widget.extra!}',
+                style: Theme.of(context).textTheme.titleMedium),
+          if (!widget.withScaffold) ...<Widget>[
+            const Padding(padding: EdgeInsets.all(16)),
+            TextButton(
+              onPressed: () {
+                GoRouter.of(context).pop();
+              },
+              child: const Text('< Back',
+                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
+            ),
+          ]
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart
index bcf426f..31ed9c8 100644
--- a/packages/go_router/lib/go_router.dart
+++ b/packages/go_router/lib/go_router.dart
@@ -7,7 +7,16 @@
 library go_router;
 
 export 'src/configuration.dart'
-    show GoRoute, GoRouterState, RouteBase, ShellRoute;
+    show
+        GoRoute,
+        GoRouterState,
+        RouteBase,
+        ShellRoute,
+        ShellNavigationContainerBuilder,
+        StatefulNavigationShell,
+        StatefulNavigationShellState,
+        StatefulShellBranch,
+        StatefulShellRoute;
 export 'src/misc/extensions.dart';
 export 'src/misc/inherited_router.dart';
 export 'src/pages/custom_transition_page.dart';
@@ -21,4 +30,11 @@
         TypedShellRoute;
 export 'src/router.dart';
 export 'src/typedefs.dart'
-    show GoRouterPageBuilder, GoRouterRedirect, GoRouterWidgetBuilder;
+    show
+        GoRouterPageBuilder,
+        GoRouterRedirect,
+        GoRouterWidgetBuilder,
+        ShellRouteBuilder,
+        ShellRoutePageBuilder,
+        StatefulShellRouteBuilder,
+        StatefulShellRoutePageBuilder;
diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart
index c497a79..f151251 100644
--- a/packages/go_router/lib/src/builder.dart
+++ b/packages/go_router/lib/src/builder.dart
@@ -5,17 +5,28 @@
 import 'package:collection/collection.dart';
 import 'package:flutter/widgets.dart';
 
+import '../go_router.dart';
 import 'configuration.dart';
 import 'logging.dart';
 import 'match.dart';
 import 'matching.dart';
 import 'misc/error_screen.dart';
 import 'pages/cupertino.dart';
-import 'pages/custom_transition_page.dart';
 import 'pages/material.dart';
 import 'route_data.dart';
 import 'typedefs.dart';
 
+/// Signature for a function that takes in a `route` to be popped with
+/// the `result` and returns a boolean decision on whether the pop
+/// is successful.
+///
+/// The `match` is the corresponding [RouteMatch] the `route`
+/// associates with.
+///
+/// Used by of [RouteBuilder.onPopPageWithRouteMatch].
+typedef PopPageWithRouteMatchCallback = bool Function(
+    Route<dynamic> route, dynamic result, RouteMatch? match);
+
 /// Builds the top-level Navigator for GoRouter.
 class RouteBuilder {
   /// [RouteBuilder] constructor.
@@ -26,6 +37,7 @@
     required this.errorBuilder,
     required this.restorationScopeId,
     required this.observers,
+    required this.onPopPageWithRouteMatch,
   });
 
   /// Builder function for a go router with Navigator.
@@ -50,19 +62,18 @@
 
   final GoRouterStateRegistry _registry = GoRouterStateRegistry();
 
-  final Map<Page<Object?>, RouteMatch> _routeMatchLookUp =
-      <Page<Object?>, RouteMatch>{};
-
-  /// Looks the the [RouteMatch] for a given [Page].
+  /// A callback called when a `route` produced by `match` is about to be popped
+  /// with the `result`.
   ///
-  /// The [Page] must be in the latest [Navigator.pages]; otherwise, this method
-  /// returns null.
-  RouteMatch? getRouteMatchForPage(Page<Object?> page) =>
-      _routeMatchLookUp[page];
+  /// If this method returns true, this builder pops the `route` and `match`.
+  ///
+  /// If this method returns false, this builder aborts the pop.
+  final PopPageWithRouteMatchCallback onPopPageWithRouteMatch;
 
-  // final Map<>
   /// Caches a HeroController for the nested Navigator, which solves cases where the
   /// Hero Widget animation stops working when navigating.
+  // TODO(chunhtai): Remove _goHeroCache once below issue is fixed:
+  // https://github.com/flutter/flutter/issues/54200
   final Map<GlobalKey<NavigatorState>, HeroController> _goHeroCache =
       <GlobalKey<NavigatorState>, HeroController>{};
 
@@ -70,7 +81,6 @@
   Widget build(
     BuildContext context,
     RouteMatchList matchList,
-    PopPageCallback onPopPage,
     bool routerNeglect,
   ) {
     if (matchList.isEmpty) {
@@ -85,14 +95,14 @@
           try {
             final Map<Page<Object?>, GoRouterState> newRegistry =
                 <Page<Object?>, GoRouterState>{};
-            final Widget result = tryBuild(context, matchList, onPopPage,
-                routerNeglect, configuration.navigatorKey, newRegistry);
+            final Widget result = tryBuild(context, matchList, routerNeglect,
+                configuration.navigatorKey, newRegistry);
             _registry.updateRegistry(newRegistry);
             return GoRouterStateRegistryScope(
                 registry: _registry, child: result);
           } on _RouteBuilderError catch (e) {
-            return _buildErrorNavigator(context, e, matchList.uri, onPopPage,
-                configuration.navigatorKey);
+            return _buildErrorNavigator(context, e, matchList.uri,
+                onPopPageWithRouteMatch, configuration.navigatorKey);
           }
         },
       ),
@@ -107,60 +117,79 @@
   Widget tryBuild(
     BuildContext context,
     RouteMatchList matchList,
-    PopPageCallback onPopPage,
     bool routerNeglect,
     GlobalKey<NavigatorState> navigatorKey,
     Map<Page<Object?>, GoRouterState> registry,
   ) {
+    // TODO(chunhtai): move the state from local scope to a central place.
+    // https://github.com/flutter/flutter/issues/126365
+    final _PagePopContext pagePopContext =
+        _PagePopContext._(onPopPageWithRouteMatch);
     return builderWithNav(
       context,
       _buildNavigator(
-        onPopPage,
-        buildPages(context, matchList, onPopPage, routerNeglect, navigatorKey,
-            registry),
+        pagePopContext.onPopPage,
+        _buildPages(context, matchList, pagePopContext, routerNeglect,
+            navigatorKey, registry),
         navigatorKey,
         observers: observers,
+        restorationScopeId: restorationScopeId,
       ),
     );
   }
 
   /// Returns the top-level pages instead of the root navigator. Used for
   /// testing.
-  @visibleForTesting
-  List<Page<Object?>> buildPages(
+  List<Page<Object?>> _buildPages(
       BuildContext context,
       RouteMatchList matchList,
-      PopPageCallback onPopPage,
+      _PagePopContext pagePopContext,
       bool routerNeglect,
       GlobalKey<NavigatorState> navigatorKey,
       Map<Page<Object?>, GoRouterState> registry) {
     final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage =
         <GlobalKey<NavigatorState>, List<Page<Object?>>>{};
     try {
-      _routeMatchLookUp.clear();
-      _buildRecursive(context, matchList, 0, onPopPage, routerNeglect,
+      _buildRecursive(context, matchList, 0, pagePopContext, routerNeglect,
           keyToPage, navigatorKey, registry);
 
       // Every Page should have a corresponding RouteMatch.
-      assert(keyToPage.values.flattened
-          .every((Page<Object?> page) => _routeMatchLookUp.containsKey(page)));
+      assert(keyToPage.values.flattened.every((Page<Object?> page) =>
+          pagePopContext.getRouteMatchForPage(page) != null));
       return keyToPage[navigatorKey]!;
     } on _RouteBuilderError catch (e) {
       return <Page<Object?>>[
         _buildErrorPage(context, e, matchList.uri),
       ];
     } finally {
-      /// Clean up previous cache to prevent memory leak.
+      /// Clean up previous cache to prevent memory leak, making sure any nested
+      /// stateful shell routes for the current match list are kept.
+      final Set<Key> activeKeys = keyToPage.keys.toSet()
+        ..addAll(_nestedStatefulNavigatorKeys(matchList));
       _goHeroCache.removeWhere(
-          (GlobalKey<NavigatorState> key, _) => !keyToPage.keys.contains(key));
+          (GlobalKey<NavigatorState> key, _) => !activeKeys.contains(key));
     }
   }
 
+  static Set<GlobalKey<NavigatorState>> _nestedStatefulNavigatorKeys(
+      RouteMatchList matchList) {
+    final StatefulShellRoute? shellRoute =
+        matchList.routes.whereType<StatefulShellRoute>().firstOrNull;
+    if (shellRoute == null) {
+      return <GlobalKey<NavigatorState>>{};
+    }
+    return RouteBase.routesRecursively(<RouteBase>[shellRoute])
+        .whereType<StatefulShellRoute>()
+        .expand((StatefulShellRoute e) =>
+            e.branches.map((StatefulShellBranch b) => b.navigatorKey))
+        .toSet();
+  }
+
   void _buildRecursive(
     BuildContext context,
     RouteMatchList matchList,
     int startIndex,
-    PopPageCallback onPopPage,
+    _PagePopContext pagePopContext,
     bool routerNeglect,
     Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPages,
     GlobalKey<NavigatorState> navigatorKey,
@@ -178,9 +207,9 @@
 
     final RouteBase route = match.route;
     final GoRouterState state = buildState(matchList, match);
+    Page<Object?>? page;
     if (route is GoRoute) {
-      final Page<Object?> page = _buildPageForRoute(context, state, match);
-      registry[page] = state;
+      page = _buildPageForGoRoute(context, state, match, route, pagePopContext);
       // If this GoRoute is for a different Navigator, add it to the
       // list of out of scope pages
       final GlobalKey<NavigatorState> goRouteNavKey =
@@ -188,68 +217,100 @@
 
       keyToPages.putIfAbsent(goRouteNavKey, () => <Page<Object?>>[]).add(page);
 
-      _buildRecursive(context, matchList, startIndex + 1, onPopPage,
+      _buildRecursive(context, matchList, startIndex + 1, pagePopContext,
           routerNeglect, keyToPages, navigatorKey, registry);
-    } else if (route is ShellRoute) {
+    } 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;
 
-      // The key to provide to the ShellRoute's Navigator.
-      final GlobalKey<NavigatorState> shellNavigatorKey = route.navigatorKey;
-
-      // The observers list for the ShellRoute's Navigator.
-      final List<NavigatorObserver> observers =
-          route.observers ?? <NavigatorObserver>[];
-
       // Add an entry for the parent navigator if none exists.
       keyToPages.putIfAbsent(parentNavigatorKey, () => <Page<Object?>>[]);
 
-      // Add an entry for the shell route's navigator
-      keyToPages.putIfAbsent(shellNavigatorKey, () => <Page<Object?>>[]);
-
       // 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;
 
+      // Get the current sub-route of this shell route from the match list.
+      final RouteBase subRoute = matchList.matches[startIndex + 1].route;
+
+      // The key to provide to the shell route's Navigator.
+      final GlobalKey<NavigatorState> shellNavigatorKey =
+          route.navigatorKeyForSubRoute(subRoute);
+
+      // Add an entry for the shell route's navigator
+      keyToPages.putIfAbsent(shellNavigatorKey, () => <Page<Object?>>[]);
+
       // Build the remaining pages
-      _buildRecursive(context, matchList, startIndex + 1, onPopPage,
+      _buildRecursive(context, matchList, startIndex + 1, pagePopContext,
           routerNeglect, keyToPages, shellNavigatorKey, registry);
 
       final HeroController heroController = _goHeroCache.putIfAbsent(
           shellNavigatorKey, () => _getHeroController(context));
-      // Build the Navigator
-      final Widget child = HeroControllerScope(
-        controller: heroController,
-        child: _buildNavigator(
-            onPopPage, keyToPages[shellNavigatorKey]!, shellNavigatorKey,
-            observers: observers),
+
+      // 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
-      final Page<Object?> page =
-          _buildPageForRoute(context, state, match, child: child);
-      registry[page] = state;
+      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;
+      pagePopContext._setRouteMatchForPage(page, match);
+    } else {
+      throw _RouteBuilderException('Unsupported route type $route');
+    }
   }
 
-  Navigator _buildNavigator(
+  static Widget _buildNavigator(
     PopPageCallback onPopPage,
     List<Page<Object?>> pages,
     Key? navigatorKey, {
     List<NavigatorObserver> observers = const <NavigatorObserver>[],
+    String? restorationScopeId,
+    HeroController? heroController,
   }) {
-    return Navigator(
+    final Widget navigator = Navigator(
       key: navigatorKey,
       restorationScopeId: restorationScopeId,
       pages: pages,
       observers: observers,
       onPopPage: onPopPage,
     );
+    if (heroController != null) {
+      return HeroControllerScope(
+        controller: heroController,
+        child: navigator,
+      );
+    } else {
+      return navigator;
+    }
   }
 
   /// Helper method that builds a [GoRouterState] object for the given [match]
@@ -282,70 +343,72 @@
     );
   }
 
-  /// Builds a [Page] for [StackedRoute]
-  Page<Object?> _buildPageForRoute(
-      BuildContext context, GoRouterState state, RouteMatch match,
-      {Widget? child}) {
-    final RouteBase route = match.route;
+  /// Builds a [Page] for [GoRoute]
+  Page<Object?> _buildPageForGoRoute(BuildContext context, GoRouterState state,
+      RouteMatch match, GoRoute route, _PagePopContext pagePopContext) {
     Page<Object?>? page;
 
-    if (route is GoRoute) {
-      // Call the pageBuilder if it's non-null
-      final GoRouterPageBuilder? pageBuilder = route.pageBuilder;
-      if (pageBuilder != null) {
-        page = pageBuilder(context, state);
-      }
-    } else if (route is ShellRoute) {
-      final ShellRoutePageBuilder? pageBuilder = route.pageBuilder;
-      assert(child != null, 'ShellRoute must contain a child route');
-      if (pageBuilder != null) {
-        page = pageBuilder(context, state, child!);
+    // Call the pageBuilder if it's non-null
+    final GoRouterPageBuilder? pageBuilder = route.pageBuilder;
+    if (pageBuilder != null) {
+      page = pageBuilder(context, state);
+      if (page is NoOpPage) {
+        page = null;
       }
     }
 
+    // Return the result of the route's builder() or pageBuilder()
+    return page ??
+        buildPage(context, state, Builder(builder: (BuildContext context) {
+          return _callGoRouteBuilder(context, state, route);
+        }));
+  }
+
+  /// Calls the user-provided route builder from the [GoRoute].
+  Widget _callGoRouteBuilder(
+      BuildContext context, GoRouterState state, GoRoute route) {
+    final GoRouterWidgetBuilder? builder = route.builder;
+
+    if (builder == null) {
+      throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route');
+    }
+
+    return builder(context, state);
+  }
+
+  /// Builds a [Page] for [ShellRouteBase]
+  Page<Object?> _buildPageForShellRoute(
+      BuildContext context,
+      GoRouterState state,
+      RouteMatch match,
+      ShellRouteBase route,
+      _PagePopContext pagePopContext,
+      ShellRouteContext shellRouteContext) {
+    Page<Object?>? page = route.buildPage(context, state, shellRouteContext);
     if (page is NoOpPage) {
       page = null;
     }
 
-    page ??= buildPage(context, state, Builder(builder: (BuildContext context) {
-      return _callRouteBuilder(context, state, match, childWidget: child);
-    }));
-    _routeMatchLookUp[page] = match;
-
     // Return the result of the route's builder() or pageBuilder()
-    return page;
+    return page ??
+        buildPage(context, state, Builder(builder: (BuildContext context) {
+          return _callShellRouteBaseBuilder(
+              context, state, route, shellRouteContext);
+        }));
   }
 
-  /// Calls the user-provided route builder from the [RouteMatch]'s [RouteBase].
-  Widget _callRouteBuilder(
-      BuildContext context, GoRouterState state, RouteMatch match,
-      {Widget? childWidget}) {
-    final RouteBase route = match.route;
-
-    if (route is GoRoute) {
-      final GoRouterWidgetBuilder? builder = route.builder;
-
-      if (builder == null) {
-        throw _RouteBuilderError('No routeBuilder provided to GoRoute: $route');
-      }
-
-      return builder(context, state);
-    } else if (route is ShellRoute) {
-      if (childWidget == null) {
-        throw _RouteBuilderException(
-            'Attempt to build ShellRoute without a child widget');
-      }
-
-      final ShellRouteBuilder? builder = route.builder;
-
-      if (builder == null) {
-        throw _RouteBuilderError('No builder provided to ShellRoute: $route');
-      }
-
-      return builder(context, state, childWidget);
+  /// Calls the user-provided route builder from the [ShellRouteBase].
+  Widget _callShellRouteBaseBuilder(BuildContext context, GoRouterState state,
+      ShellRouteBase route, ShellRouteContext? shellRouteContext) {
+    assert(shellRouteContext != null,
+        'ShellRouteContext must be provided for ${route.runtimeType}');
+    final Widget? widget =
+        route.buildWidget(context, state, shellRouteContext!);
+    if (widget == null) {
+      throw _RouteBuilderError('No builder provided to ShellRoute: $route');
     }
 
-    throw _RouteBuilderException('Unsupported route type $route');
+    return widget;
   }
 
   _PageBuilderForAppType? _pageBuilderForAppType;
@@ -427,10 +490,10 @@
       BuildContext context,
       _RouteBuilderError e,
       Uri uri,
-      PopPageCallback onPopPage,
+      PopPageWithRouteMatchCallback onPopPage,
       GlobalKey<NavigatorState> navigatorKey) {
     return _buildNavigator(
-      onPopPage,
+      (Route<dynamic> route, dynamic result) => onPopPage(route, result, null),
       <Page<Object?>>[
         _buildErrorPage(context, e, uri),
       ],
@@ -529,3 +592,33 @@
     return '$message ${exception ?? ""}';
   }
 }
+
+/// Context used to provide a route to page association when popping routes.
+class _PagePopContext {
+  _PagePopContext._(this.onPopPageWithRouteMatch);
+
+  final Map<Page<dynamic>, RouteMatch> _routeMatchLookUp =
+      <Page<Object?>, RouteMatch>{};
+
+  /// On pop page callback that includes the associated [RouteMatch].
+  final PopPageWithRouteMatchCallback onPopPageWithRouteMatch;
+
+  /// Looks for the [RouteMatch] for a given [Page].
+  ///
+  /// The [Page] must have been previously built via the [RouteBuilder] that
+  /// created this [PagePopContext]; otherwise, this method returns null.
+  RouteMatch? getRouteMatchForPage(Page<Object?> page) =>
+      _routeMatchLookUp[page];
+
+  void _setRouteMatchForPage(Page<Object?> page, RouteMatch match) =>
+      _routeMatchLookUp[page] = match;
+
+  /// Function used as [Navigator.onPopPage] callback when creating Navigators.
+  ///
+  /// This function forwards to [onPopPageWithRouteMatch], including the
+  /// [RouteMatch] associated with the popped route.
+  bool onPopPage(Route<dynamic> route, dynamic result) {
+    final Page<Object?> page = route.settings as Page<Object?>;
+    return onPopPageWithRouteMatch(route, result, _routeMatchLookUp[page]);
+  }
+}
diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart
index 2c2f8e7..07d02d4 100644
--- a/packages/go_router/lib/src/configuration.dart
+++ b/packages/go_router/lib/src/configuration.dart
@@ -6,6 +6,7 @@
 
 import 'configuration.dart';
 import 'logging.dart';
+import 'matching.dart';
 import 'misc/errors.dart';
 import 'path_utils.dart';
 import 'typedefs.dart';
@@ -25,6 +26,8 @@
             _debugVerifyNoDuplicatePathParameter(routes, <String, GoRoute>{})),
         assert(_debugCheckParentNavigatorKeys(
             routes, <GlobalKey<NavigatorState>>[navigatorKey])) {
+    assert(_debugCheckStatefulShellBranchDefaultLocations(
+        routes, RouteMatcher(this)));
     _cacheNameToPath('', routes);
     log.info(debugKnownRoutes());
   }
@@ -41,7 +44,7 @@
               'sub-route path may not start or end with /: $route');
         }
         subRouteIsTopLevel = false;
-      } else if (route is ShellRoute) {
+      } else if (route is ShellRouteBase) {
         subRouteIsTopLevel = isTopLevel;
       }
       _debugCheckPath(route.routes, subRouteIsTopLevel);
@@ -86,6 +89,21 @@
           route.routes,
           <GlobalKey<NavigatorState>>[...allowedKeys..add(route.navigatorKey)],
         );
+      } else if (route is StatefulShellRoute) {
+        for (final StatefulShellBranch branch in route.branches) {
+          assert(
+              !allowedKeys.contains(branch.navigatorKey),
+              'StatefulShellBranch must not reuse an ancestor navigatorKey '
+              '(${branch.navigatorKey})');
+
+          _debugCheckParentNavigatorKeys(
+            branch.routes,
+            <GlobalKey<NavigatorState>>[
+              ...allowedKeys,
+              branch.navigatorKey,
+            ],
+          );
+        }
       }
     }
     return true;
@@ -111,6 +129,56 @@
     return true;
   }
 
+  // Check to see that the configured initialLocation of StatefulShellBranches
+  // points to a descendant route of the route branch.
+  bool _debugCheckStatefulShellBranchDefaultLocations(
+      List<RouteBase> routes, RouteMatcher matcher) {
+    try {
+      for (final RouteBase route in routes) {
+        if (route is StatefulShellRoute) {
+          for (final StatefulShellBranch branch in route.branches) {
+            if (branch.initialLocation == null) {
+              // Recursively search for the first GoRoute descendant. Will
+              // throw assertion error if not found.
+              final GoRoute? route = branch.defaultRoute;
+              final String? initialLocation =
+                  route != null ? locationForRoute(route) : null;
+              assert(
+                  initialLocation != null,
+                  'The default location of a StatefulShellBranch must be '
+                  'derivable from GoRoute descendant');
+              assert(
+                  route!.pathParameters.isEmpty,
+                  'The default location of a StatefulShellBranch cannot be '
+                  'a parameterized route');
+            } else {
+              final List<RouteBase> matchRoutes =
+                  matcher.findMatch(branch.initialLocation!).routes;
+              final int shellIndex = matchRoutes.indexOf(route);
+              bool matchFound = false;
+              if (shellIndex >= 0 && (shellIndex + 1) < matchRoutes.length) {
+                final RouteBase branchRoot = matchRoutes[shellIndex + 1];
+                matchFound = branch.routes.contains(branchRoot);
+              }
+              assert(
+                  matchFound,
+                  'The initialLocation (${branch.initialLocation}) of '
+                  'StatefulShellBranch must match a descendant route of the '
+                  'branch');
+            }
+          }
+        }
+        _debugCheckStatefulShellBranchDefaultLocations(route.routes, matcher);
+      }
+    } on MatcherError catch (e) {
+      assert(
+          false,
+          'initialLocation (${e.location}) of StatefulShellBranch must '
+          'be a valid location');
+    }
+    return true;
+  }
+
   /// The list of top level routes used by [GoRouterDelegate].
   final List<RouteBase> routes;
 
@@ -167,6 +235,13 @@
         .toString();
   }
 
+  /// Get the location for the provided route.
+  ///
+  /// Builds the absolute path for the route, by concatenating the paths of the
+  /// route and all its ancestors.
+  String? locationForRoute(RouteBase route) =>
+      fullPathForRoute(route, '', routes);
+
   @override
   String toString() {
     return 'RouterConfiguration: $routes';
@@ -222,7 +297,7 @@
         if (route.routes.isNotEmpty) {
           _cacheNameToPath(fullPath, route.routes);
         }
-      } else if (route is ShellRoute) {
+      } else if (route is ShellRouteBase) {
         if (route.routes.isNotEmpty) {
           _cacheNameToPath(parentFullPath, route.routes);
         }
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index a62906a..985dce3 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -27,25 +27,29 @@
     required List<NavigatorObserver> observers,
     required this.routerNeglect,
     String? restorationScopeId,
-  })  : _configuration = configuration,
-        builder = RouteBuilder(
-          configuration: configuration,
-          builderWithNav: builderWithNav,
-          errorPageBuilder: errorPageBuilder,
-          errorBuilder: errorBuilder,
-          restorationScopeId: restorationScopeId,
-          observers: observers,
-        );
+  }) : _configuration = configuration {
+    builder = RouteBuilder(
+      configuration: configuration,
+      builderWithNav: builderWithNav,
+      errorPageBuilder: errorPageBuilder,
+      errorBuilder: errorBuilder,
+      restorationScopeId: restorationScopeId,
+      observers: observers,
+      onPopPageWithRouteMatch: _handlePopPageWithRouteMatch,
+    );
+  }
 
   /// Builds the top-level Navigator given a configuration and location.
   @visibleForTesting
-  final RouteBuilder builder;
+  late final RouteBuilder builder;
 
   /// Set to true to disable creating history entries on the web.
   final bool routerNeglect;
 
   RouteMatchList _matchList = RouteMatchList.empty;
 
+  final RouteConfiguration _configuration;
+
   /// Stores the number of times each route route has been pushed.
   ///
   /// This is used to generate a unique key for each route.
@@ -58,7 +62,6 @@
   /// }
   /// ```
   final Map<String, int> _pushCounts = <String, int>{};
-  final RouteConfiguration _configuration;
 
   _NavigatorStateIterator _createNavigatorStateIterator() =>
       _NavigatorStateIterator(_matchList, navigatorKey.currentState!);
@@ -85,10 +88,6 @@
   Future<T?> _push<T extends Object?>(
       RouteMatchList matches, ValueKey<String> pageKey) async {
     final ImperativeRouteMatch<T> newPageKeyMatch = ImperativeRouteMatch<T>(
-      route: matches.last.route,
-      matchedLocation: matches.last.matchedLocation,
-      extra: matches.last.extra,
-      error: matches.last.error,
       pageKey: pageKey,
       matches: matches,
     );
@@ -149,12 +148,11 @@
     );
   }
 
-  bool _onPopPage(Route<Object?> route, Object? result) {
+  bool _handlePopPageWithRouteMatch(
+      Route<Object?> route, Object? result, RouteMatch? match) {
     if (!route.didPop(result)) {
       return false;
     }
-    final Page<Object?> page = route.settings as Page<Object?>;
-    final RouteMatch? match = builder.getRouteMatchForPage(page);
     assert(match != null);
     if (match is ImperativeRouteMatch) {
       match.complete(result);
@@ -219,7 +217,6 @@
     return builder.build(
       context,
       _matchList,
-      _onPopPage,
       routerNeglect,
     );
   }
@@ -257,6 +254,7 @@
     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;
@@ -294,19 +292,22 @@
 
         current = parentNavigatorKey.currentState!;
         return true;
-      } else if (route is ShellRoute) {
+      } else if (route is ShellRouteBase) {
         // 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(route.navigatorKey.currentContext!)!;
+            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;
         }
-        current = route.navigatorKey.currentState!;
+        current = navigatorKey.currentState!;
         return true;
       }
+      subRoute = route;
     }
     assert(index == -1);
     current = root;
diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart
index 7c3c736..24aa058 100644
--- a/packages/go_router/lib/src/match.dart
+++ b/packages/go_router/lib/src/match.dart
@@ -36,7 +36,7 @@
     required Map<String, String> pathParameters,
     required Object? extra,
   }) {
-    if (route is ShellRoute) {
+    if (route is ShellRouteBase) {
       return RouteMatch(
         route: route,
         matchedLocation: remainingLocation,
@@ -112,13 +112,15 @@
 class ImperativeRouteMatch<T> extends RouteMatch {
   /// Constructor for [ImperativeRouteMatch].
   ImperativeRouteMatch({
-    required super.route,
-    required super.matchedLocation,
-    required super.extra,
-    required super.error,
     required super.pageKey,
     required this.matches,
-  }) : _completer = Completer<T?>();
+  })  : _completer = Completer<T?>(),
+        super(
+          route: matches.last.route,
+          matchedLocation: matches.last.matchedLocation,
+          extra: matches.last.extra,
+          error: matches.last.error,
+        );
 
   /// The matches that produces this route match.
   final RouteMatchList matches;
diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart
index 5855204..b809cca 100644
--- a/packages/go_router/lib/src/matching.dart
+++ b/packages/go_router/lib/src/matching.dart
@@ -158,7 +158,7 @@
     newMatches.removeRange(index, newMatches.length);
 
     // Also pop ShellRoutes when there are no subsequent route matches
-    while (newMatches.isNotEmpty && newMatches.last.route is ShellRoute) {
+    while (newMatches.isNotEmpty && newMatches.last.route is ShellRouteBase) {
       newMatches.removeLast();
     }
     // Removing ImperativeRouteMatch should not change uri and pathParameters.
@@ -193,10 +193,13 @@
   RouteMatch get last => matches.last;
 
   /// Returns true if the current match intends to display an error screen.
-  bool get isError => matches.length == 1 && matches.first.error != null;
+  bool get isError => error != null;
 
   /// Returns the error that this match intends to display.
-  Exception? get error => matches.first.error;
+  Exception? get error => matches.firstOrNull?.error;
+
+  /// The routes for each of the matches.
+  List<RouteBase> get routes => matches.map((RouteMatch e) => e.route).toList();
 
   RouteMatchList _copyWith({
     List<RouteMatch>? matches,
@@ -237,6 +240,115 @@
   String toString() {
     return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)';
   }
+
+  /// Returns a pre-parsed [RouteInformation], containing a reference to this
+  /// match list.
+  RouteInformation toPreParsedRouteInformation() {
+    return RouteInformation(
+      // TODO(tolo): remove this ignore and migrate the code
+      // https://github.com/flutter/flutter/issues/124045.
+      // ignore: deprecated_member_use
+      location: uri.toString(),
+      state: this,
+    );
+  }
+
+  /// Attempts to extract a pre-parsed match list from the provided
+  /// [RouteInformation].
+  static RouteMatchList? fromPreParsedRouteInformation(
+      RouteInformation routeInformation) {
+    if (routeInformation.state is RouteMatchList) {
+      return routeInformation.state! as RouteMatchList;
+    }
+    return null;
+  }
+}
+
+/// Handles encoding and decoding of [RouteMatchList] objects to a format
+/// suitable for using with [StandardMessageCodec].
+///
+/// The primary use of this class is for state restoration.
+class RouteMatchListCodec {
+  /// Creates a new [RouteMatchListCodec] object.
+  RouteMatchListCodec(this._matcher);
+
+  static const String _encodedDataKey = 'go_router/encoded_route_match_list';
+  static const String _locationKey = 'location';
+  static const String _stateKey = 'state';
+  static const String _imperativeMatchesKey = 'imperativeMatches';
+  static const String _pageKey = 'pageKey';
+
+  final RouteMatcher _matcher;
+
+  /// Encodes the provided [RouteMatchList].
+  Object? encodeMatchList(RouteMatchList matchlist) {
+    if (matchlist.isEmpty) {
+      return null;
+    }
+    final List<Map<Object?, Object?>> imperativeMatches = matchlist.matches
+        .whereType<ImperativeRouteMatch<Object?>>()
+        .map((ImperativeRouteMatch<Object?> e) => _toPrimitives(
+            e.matches.uri.toString(), e.extra,
+            pageKey: e.pageKey.value))
+        .toList();
+
+    return <Object?, Object?>{
+      _encodedDataKey: _toPrimitives(
+          matchlist.uri.toString(), matchlist.matches.first.extra,
+          imperativeMatches: imperativeMatches),
+    };
+  }
+
+  static Map<Object?, Object?> _toPrimitives(String location, Object? state,
+      {List<dynamic>? imperativeMatches, String? pageKey}) {
+    return <Object?, Object?>{
+      _locationKey: location,
+      _stateKey: state,
+      if (imperativeMatches != null) _imperativeMatchesKey: imperativeMatches,
+      if (pageKey != null) _pageKey: pageKey,
+    };
+  }
+
+  /// Attempts to decode the provided object into a [RouteMatchList].
+  RouteMatchList? decodeMatchList(Object? object) {
+    if (object is Map && object[_encodedDataKey] is Map) {
+      final Map<Object?, Object?> data =
+          object[_encodedDataKey] as Map<Object?, Object?>;
+      final Object? rootLocation = data[_locationKey];
+      if (rootLocation is! String) {
+        return null;
+      }
+      final RouteMatchList matchList =
+          _matcher.findMatch(rootLocation, extra: data[_stateKey]);
+
+      final List<Object?>? imperativeMatches =
+          data[_imperativeMatchesKey] as List<Object?>?;
+      if (imperativeMatches != null) {
+        for (int i = 0; i < imperativeMatches.length; i++) {
+          final Object? match = imperativeMatches[i];
+          if (match is! Map ||
+              match[_locationKey] is! String ||
+              match[_pageKey] is! String) {
+            continue;
+          }
+          final ValueKey<String> pageKey =
+              ValueKey<String>(match[_pageKey] as String);
+          final RouteMatchList imperativeMatchList = _matcher.findMatch(
+              match[_locationKey] as String,
+              extra: match[_stateKey]);
+          final ImperativeRouteMatch<Object?> imperativeMatch =
+              ImperativeRouteMatch<Object?>(
+            pageKey: pageKey,
+            matches: imperativeMatchList,
+          );
+          matchList.push(imperativeMatch);
+        }
+      }
+
+      return matchList;
+    }
+    return null;
+  }
 }
 
 /// An error that occurred during matching.
@@ -307,7 +419,7 @@
       // Otherwise, recurse
       final String childRestLoc;
       final String newParentSubLoc;
-      if (match.route is ShellRoute) {
+      if (match.route is ShellRouteBase) {
         childRestLoc = remainingLocation;
         newParentSubLoc = matchedLocation;
       } else {
diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart
index 7fd00cf..78a7ee0 100644
--- a/packages/go_router/lib/src/parser.dart
+++ b/packages/go_router/lib/src/parser.dart
@@ -57,11 +57,17 @@
   ) {
     late final RouteMatchList initialMatches;
     try {
-      // TODO(chunhtai): remove this ignore and migrate the code
-      // https://github.com/flutter/flutter/issues/124045.
-      // ignore: deprecated_member_use, unnecessary_non_null_assertion
-      initialMatches = matcher.findMatch(routeInformation.location!,
-          extra: routeInformation.state);
+      final RouteMatchList? preParsedMatchList =
+          RouteMatchList.fromPreParsedRouteInformation(routeInformation);
+      if (preParsedMatchList != null) {
+        initialMatches = preParsedMatchList;
+      } else {
+        // TODO(chunhtai): remove this ignore and migrate the code
+        // https://github.com/flutter/flutter/issues/124045.
+        // ignore: deprecated_member_use, unnecessary_non_null_assertion
+        initialMatches = matcher.findMatch(routeInformation.location!,
+            extra: routeInformation.state);
+      }
     } on MatcherError {
       // TODO(chunhtai): remove this ignore and migrate the code
       // https://github.com/flutter/flutter/issues/124045.
diff --git a/packages/go_router/lib/src/path_utils.dart b/packages/go_router/lib/src/path_utils.dart
index e9db923..ba403f4 100644
--- a/packages/go_router/lib/src/path_utils.dart
+++ b/packages/go_router/lib/src/path_utils.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 '../go_router.dart';
+
 final RegExp _parameterRegExp = RegExp(r':(\w+)(\((?:\\.|[^\\()])+\))?');
 
 /// Converts a [pattern] such as `/user/:id` into [RegExp].
@@ -135,3 +137,24 @@
 
   return canon;
 }
+
+/// Builds an absolute path for the provided route.
+String? fullPathForRoute(
+    RouteBase targetRoute, String parentFullpath, List<RouteBase> routes) {
+  for (final RouteBase route in routes) {
+    final String fullPath = (route is GoRoute)
+        ? concatenatePaths(parentFullpath, route.path)
+        : parentFullpath;
+
+    if (route == targetRoute) {
+      return fullPath;
+    } else {
+      final String? subRoutePath =
+          fullPathForRoute(targetRoute, fullPath, route.routes);
+      if (subRoutePath != null) {
+        return subRoutePath;
+      }
+    }
+  }
+  return null;
+}
diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart
index 21e8053..69b27e3 100644
--- a/packages/go_router/lib/src/route.dart
+++ b/packages/go_router/lib/src/route.dart
@@ -2,11 +2,13 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:collection/collection.dart';
 import 'package:flutter/widgets.dart';
 import 'package:meta/meta.dart';
 
-import 'configuration.dart';
-import 'pages/custom_transition_page.dart';
+import '../go_router.dart';
+import 'match.dart';
+import 'matching.dart';
 import 'path_utils.dart';
 import 'typedefs.dart';
 
@@ -101,6 +103,13 @@
 
   /// The list of child routes associated with this route.
   final List<RouteBase> routes;
+
+  /// Builds a lists containing the provided routes along with all their
+  /// descendant [routes].
+  static Iterable<RouteBase> routesRecursively(Iterable<RouteBase> routes) {
+    return routes.expand(
+        (RouteBase e) => <RouteBase>[e, ...routesRecursively(e.routes)]);
+  }
 }
 
 /// A route that is displayed visually above the matching parent route using the
@@ -320,6 +329,72 @@
   late final RegExp _pathRE;
 }
 
+/// Base class for classes that act as shells for sub-routes, such
+/// as [ShellRoute] and [StatefulShellRoute].
+abstract class ShellRouteBase extends RouteBase {
+  /// Constructs a [ShellRouteBase].
+  const ShellRouteBase._({super.routes}) : super._();
+
+  /// Attempts to build the Widget representing this shell route.
+  ///
+  /// Returns null if this shell route does not build a Widget, but instead uses
+  /// a Page to represent itself (see [buildPage]).
+  Widget? buildWidget(BuildContext context, GoRouterState state,
+      ShellRouteContext shellRouteContext);
+
+  /// Attempts to build the Page representing this shell route.
+  ///
+  /// Returns null if this shell route does not build a Page, but instead uses
+  /// a Widget to represent itself (see [buildWidget]).
+  Page<dynamic>? buildPage(BuildContext context, GoRouterState state,
+      ShellRouteContext shellRouteContext);
+
+  /// Returns the key for the [Navigator] that is to be used for the specified
+  /// immediate sub-route of this shell route.
+  GlobalKey<NavigatorState> navigatorKeyForSubRoute(RouteBase subRoute);
+
+  /// Returns the keys for the [Navigator]s associated with this shell route.
+  Iterable<GlobalKey<NavigatorState>> get _navigatorKeys =>
+      <GlobalKey<NavigatorState>>[];
+
+  /// Returns all the Navigator keys of this shell route as well as those of any
+  /// descendant shell routes.
+  Iterable<GlobalKey<NavigatorState>> _navigatorKeysRecursively() {
+    return RouteBase.routesRecursively(<ShellRouteBase>[this])
+        .whereType<ShellRouteBase>()
+        .expand((ShellRouteBase e) => e._navigatorKeys);
+  }
+}
+
+/// Context object used when building the shell and Navigator for a shell route.
+class ShellRouteContext {
+  /// Constructs a [ShellRouteContext].
+  ShellRouteContext({
+    required this.route,
+    required this.routerState,
+    required this.navigatorKey,
+    required this.routeMatchList,
+    required this.navigatorBuilder,
+  });
+
+  /// The associated shell route.
+  final ShellRouteBase route;
+
+  /// The current route state associated with [route].
+  final GoRouterState routerState;
+
+  /// The [Navigator] key to be used for the nested navigation associated with
+  /// [route].
+  final GlobalKey<NavigatorState> navigatorKey;
+
+  /// The route match list representing the current location within the
+  /// associated shell route.
+  final RouteMatchList routeMatchList;
+
+  /// Function used to build the [Navigator] for the current route.
+  final NavigatorBuilder navigatorBuilder;
+}
+
 /// A route that displays a UI shell around the matching child route.
 ///
 /// When a ShellRoute is added to the list of routes on GoRouter or GoRoute, a
@@ -415,7 +490,7 @@
 /// ```
 ///
 /// {@category Configuration}
-class ShellRoute extends RouteBase {
+class ShellRoute extends ShellRouteBase {
   /// Constructs a [ShellRoute].
   ShellRoute({
     this.builder,
@@ -423,6 +498,7 @@
     this.observers,
     super.routes,
     GlobalKey<NavigatorState>? navigatorKey,
+    this.restorationScopeId,
   })  : assert(routes.isNotEmpty),
         navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>(),
         super._() {
@@ -436,18 +512,42 @@
 
   /// The widget builder for a shell route.
   ///
-  /// Similar to GoRoute builder, but with an additional child parameter. This
-  /// child parameter is the Widget built by calling the matching sub-route's
-  /// builder.
+  /// Similar to [GoRoute.builder], but with an additional child parameter. This
+  /// child parameter is the Widget managing the nested navigation for the
+  /// matching sub-routes. Typically, a shell route builds its shell around this
+  /// Widget.
   final ShellRouteBuilder? builder;
 
   /// The page builder for a shell route.
   ///
-  /// Similar to GoRoute pageBuilder, but with an additional child parameter.
-  /// This child parameter is the Widget built by calling the matching
-  /// sub-route's builder.
+  /// Similar to [GoRoute.pageBuilder], but with an additional child parameter.
+  /// This child parameter is the Widget managing the nested navigation for the
+  /// matching sub-routes. Typically, a shell route builds its shell around this
+  /// Widget.
   final ShellRoutePageBuilder? pageBuilder;
 
+  @override
+  Widget? buildWidget(BuildContext context, GoRouterState state,
+      ShellRouteContext shellRouteContext) {
+    if (builder != null) {
+      final Widget navigator =
+          shellRouteContext.navigatorBuilder(observers, restorationScopeId);
+      return builder!(context, state, navigator);
+    }
+    return null;
+  }
+
+  @override
+  Page<dynamic>? buildPage(BuildContext context, GoRouterState state,
+      ShellRouteContext shellRouteContext) {
+    if (pageBuilder != null) {
+      final Widget navigator =
+          shellRouteContext.navigatorBuilder(observers, restorationScopeId);
+      return pageBuilder!(context, state, navigator);
+    }
+    return null;
+  }
+
   /// The observers for a shell route.
   ///
   /// The observers parameter is used by the [Navigator] built for this route.
@@ -458,4 +558,720 @@
   /// All ShellRoutes build a Navigator by default. Child GoRoutes
   /// are placed onto this Navigator instead of the root Navigator.
   final GlobalKey<NavigatorState> navigatorKey;
+
+  /// Restoration ID to save and restore the state of the navigator, including
+  /// its history.
+  final String? restorationScopeId;
+
+  @override
+  GlobalKey<NavigatorState> navigatorKeyForSubRoute(RouteBase subRoute) {
+    assert(routes.contains(subRoute));
+    return navigatorKey;
+  }
+
+  @override
+  Iterable<GlobalKey<NavigatorState>> get _navigatorKeys =>
+      <GlobalKey<NavigatorState>>[navigatorKey];
+}
+
+/// A route that displays a UI shell with separate [Navigator]s for its
+/// sub-routes.
+///
+/// Similar to [ShellRoute], this route class places its sub-route on a
+/// different Navigator than the root [Navigator]. However, this route class
+/// differs in that it creates separate [Navigator]s for each of its nested
+/// branches (i.e. parallel navigation trees), making it possible to build an
+/// app with stateful nested navigation. This is convenient when for instance
+/// implementing a UI with a [BottomNavigationBar], with a persistent navigation
+/// state for each tab.
+///
+/// A StatefulShellRoute is created by specifying a List of
+/// [StatefulShellBranch] items, each representing a separate stateful branch
+/// in the route tree. StatefulShellBranch provides the root routes and the
+/// Navigator key ([GlobalKey]) for the branch, as well as an optional initial
+/// location.
+///
+/// Like [ShellRoute], either a [builder] or a [pageBuilder] must be provided
+/// when creating a StatefulShellRoute. However, these builders differ slightly
+/// in that they accept a [StatefulNavigationShell] parameter instead of a
+/// child Widget. The StatefulNavigationShell can be used to access information
+/// about the state of the route, as well as to switch the active branch (i.e.
+/// restoring the navigation stack of another branch). The latter is
+/// accomplished by using the method [StatefulNavigationShell.goBranch], for
+/// example:
+///
+/// ```
+/// void _onItemTapped(int index) {
+///   navigationShell.goBranch(index: index);
+/// }
+/// ```
+///
+/// The StatefulNavigationShell is also responsible for managing and maintaining
+/// the state of the branch Navigators. Typically, a shell is built around this
+/// Widget, for example by using it as the body of [Scaffold] with a
+/// [BottomNavigationBar].
+///
+/// When creating a StatefulShellRoute, a [navigatorContainerBuilder] function
+/// must be provided. This function is responsible for building the actual
+/// container for the Widgets representing the branch Navigators. Typically,
+/// the Widget returned by this function handles the layout (including
+/// [Offstage] handling etc) of the branch Navigators and any animations needed
+/// when switching active branch.
+///
+/// For a default implementation of [navigatorContainerBuilder] that is
+/// appropriate for most use cases, consider using the constructor
+/// [StatefulShellRoute.indexedStack].
+///
+/// With StatefulShellRoute (and any route below it), animated transitions
+/// between routes in the same navigation stack works the same way as with other
+/// route classes, and can be customized using pageBuilder. However, since
+/// StatefulShellRoute maintains a set of parallel navigation stacks,
+/// any transitions when switching between branches is the responsibility of the
+/// branch Navigator container (i.e. [navigatorContainerBuilder]). The default
+/// [IndexedStack] implementation ([StatefulShellRoute.indexedStack]) does not
+/// use animated transitions, but an example is provided on how to accomplish
+/// this (see link to custom StatefulShellRoute example below).
+///
+/// See also:
+/// * [StatefulShellRoute.indexedStack] which provides a default
+/// StatefulShellRoute implementation suitable for most use cases.
+/// * [Stateful Nested Navigation example](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart)
+/// for a complete runnable example using StatefulShellRoute.
+/// * [Custom StatefulShellRoute example](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/others/custom_stateful_shell_route.dart)
+/// which demonstrates how to customize the container for the branch Navigators
+/// and how to implement animated transitions when switching branches.
+class StatefulShellRoute extends ShellRouteBase {
+  /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es,
+  /// each representing a separate nested navigation tree (branch).
+  ///
+  /// A separate [Navigator] will be created for each of the branches, using
+  /// the navigator key specified in [StatefulShellBranch]. The Widget
+  /// implementing the container for the branch Navigators is provided by
+  /// [navigatorContainerBuilder].
+  StatefulShellRoute({
+    required this.branches,
+    this.builder,
+    this.pageBuilder,
+    required this.navigatorContainerBuilder,
+    this.restorationScopeId,
+  })  : assert(branches.isNotEmpty),
+        assert((pageBuilder != null) ^ (builder != null),
+            'One of builder or pageBuilder must be provided, but not both'),
+        assert(_debugUniqueNavigatorKeys(branches).length == branches.length,
+            'Navigator keys must be unique'),
+        assert(_debugValidateParentNavigatorKeys(branches)),
+        assert(_debugValidateRestorationScopeIds(restorationScopeId, branches)),
+        super._(routes: _routes(branches));
+
+  /// Constructs a StatefulShellRoute that uses an [IndexedStack] for its
+  /// nested [Navigator]s.
+  ///
+  /// This constructor provides an IndexedStack based implementation for the
+  /// container ([navigatorContainerBuilder]) used to manage the Widgets
+  /// representing the branch Navigators. A part from that, this constructor
+  /// works the same way as the default constructor.
+  ///
+  /// See [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stacked_shell_route.dart)
+  /// for a complete runnable example using StatefulShellRoute.indexedStack.
+  StatefulShellRoute.indexedStack({
+    required List<StatefulShellBranch> branches,
+    StatefulShellRouteBuilder? builder,
+    StatefulShellRoutePageBuilder? pageBuilder,
+    String? restorationScopeId,
+  }) : this(
+          branches: branches,
+          builder: builder,
+          pageBuilder: pageBuilder,
+          restorationScopeId: restorationScopeId,
+          navigatorContainerBuilder: _indexedStackContainerBuilder,
+        );
+
+  /// Restoration ID to save and restore the state of the navigator, including
+  /// its history.
+  final String? restorationScopeId;
+
+  /// The widget builder for a stateful shell route.
+  ///
+  /// Similar to [GoRoute.builder], but with an additional
+  /// [StatefulNavigationShell] parameter. StatefulNavigationShell is a Widget
+  /// responsible for managing the nested navigation for the
+  /// matching sub-routes. Typically, a shell route builds its shell around this
+  /// Widget. StatefulNavigationShell can also be used to access information
+  /// about which branch is active, and also to navigate to a different branch
+  /// (using [StatefulNavigationShell.goBranch]).
+  ///
+  /// Custom implementations may choose to ignore the child parameter passed to
+  /// the builder function, and instead use [StatefulNavigationShell] to
+  /// create a custom container for the branch Navigators.
+  final StatefulShellRouteBuilder? builder;
+
+  /// The page builder for a stateful shell route.
+  ///
+  /// Similar to [GoRoute.pageBuilder], but with an additional
+  /// [StatefulNavigationShell] parameter. StatefulNavigationShell is a Widget
+  /// responsible for managing the nested navigation for the
+  /// matching sub-routes. Typically, a shell route builds its shell around this
+  /// Widget. StatefulNavigationShell can also be used to access information
+  /// about which branch is active, and also to navigate to a different branch
+  /// (using [StatefulNavigationShell.goBranch]).
+  ///
+  /// Custom implementations may choose to ignore the child parameter passed to
+  /// the builder function, and instead use [StatefulNavigationShell] to
+  /// create a custom container for the branch Navigators.
+  final StatefulShellRoutePageBuilder? pageBuilder;
+
+  /// The builder for the branch Navigator container.
+  ///
+  /// The function responsible for building the container for the branch
+  /// Navigators. When this function is invoked, access is provided to a List of
+  /// Widgets representing the branch Navigators, where the the index
+  /// corresponds to the index of in [branches].
+  ///
+  /// The builder function is expected to return a Widget that ensures that the
+  /// state of the branch Widgets is maintained, for instance by inducting them
+  /// in the Widget tree.
+  final ShellNavigationContainerBuilder navigatorContainerBuilder;
+
+  /// Representations of the different stateful route branches that this
+  /// shell route will manage.
+  ///
+  /// Each branch uses a separate [Navigator], identified
+  /// [StatefulShellBranch.navigatorKey].
+  final List<StatefulShellBranch> branches;
+
+  final GlobalKey<StatefulNavigationShellState> _shellStateKey =
+      GlobalKey<StatefulNavigationShellState>();
+
+  @override
+  Widget? buildWidget(BuildContext context, GoRouterState state,
+      ShellRouteContext shellRouteContext) {
+    if (builder != null) {
+      return builder!(context, state, _createShell(context, shellRouteContext));
+    }
+    return null;
+  }
+
+  @override
+  Page<dynamic>? buildPage(BuildContext context, GoRouterState state,
+      ShellRouteContext shellRouteContext) {
+    if (pageBuilder != null) {
+      return pageBuilder!(
+          context, state, _createShell(context, shellRouteContext));
+    }
+    return null;
+  }
+
+  @override
+  GlobalKey<NavigatorState> navigatorKeyForSubRoute(RouteBase subRoute) {
+    final StatefulShellBranch? branch = branches.firstWhereOrNull(
+        (StatefulShellBranch e) => e.routes.contains(subRoute));
+    assert(branch != null);
+    return branch!.navigatorKey;
+  }
+
+  @override
+  Iterable<GlobalKey<NavigatorState>> get _navigatorKeys =>
+      branches.map((StatefulShellBranch b) => b.navigatorKey);
+
+  StatefulNavigationShell _createShell(
+          BuildContext context, ShellRouteContext shellRouteContext) =>
+      StatefulNavigationShell(
+          shellRouteContext: shellRouteContext,
+          router: GoRouter.of(context),
+          containerBuilder: navigatorContainerBuilder);
+
+  static Widget _indexedStackContainerBuilder(BuildContext context,
+      StatefulNavigationShell navigationShell, List<Widget> children) {
+    return _IndexedStackedRouteBranchContainer(
+        currentIndex: navigationShell.currentIndex, children: children);
+  }
+
+  static List<RouteBase> _routes(List<StatefulShellBranch> branches) =>
+      branches.expand((StatefulShellBranch e) => e.routes).toList();
+
+  static Set<GlobalKey<NavigatorState>> _debugUniqueNavigatorKeys(
+          List<StatefulShellBranch> branches) =>
+      Set<GlobalKey<NavigatorState>>.from(
+          branches.map((StatefulShellBranch e) => e.navigatorKey));
+
+  static bool _debugValidateParentNavigatorKeys(
+      List<StatefulShellBranch> branches) {
+    for (final StatefulShellBranch branch in branches) {
+      for (final RouteBase route in branch.routes) {
+        if (route is GoRoute) {
+          assert(route.parentNavigatorKey == null ||
+              route.parentNavigatorKey == branch.navigatorKey);
+        }
+      }
+    }
+    return true;
+  }
+
+  static bool _debugValidateRestorationScopeIds(
+      String? restorationScopeId, List<StatefulShellBranch> branches) {
+    if (branches
+        .map((StatefulShellBranch e) => e.restorationScopeId)
+        .whereNotNull()
+        .isNotEmpty) {
+      assert(
+          restorationScopeId != null,
+          'A restorationScopeId must be set for '
+          'the StatefulShellRoute when using restorationScopeIds on one or more '
+          'of the branches');
+    }
+    return true;
+  }
+}
+
+/// Representation of a separate branch in a stateful navigation tree, used to
+/// configure [StatefulShellRoute].
+///
+/// The only required argument when creating a StatefulShellBranch is the
+/// sub-routes ([routes]), however sometimes it may be convenient to also
+/// provide a [initialLocation]. The value of this parameter is used when
+/// loading the branch for the first time (for instance when switching branch
+/// using the goBranch method in [StatefulNavigationShell]).
+///
+/// A separate [Navigator] will be built for each StatefulShellBranch in a
+/// [StatefulShellRoute], and the routes of this branch will be placed onto that
+/// Navigator instead of the root Navigator. A custom [navigatorKey] can be
+/// provided when creating a StatefulShellBranch, which can be useful when the
+/// Navigator needs to be accessed elsewhere. If no key is provided, a default
+/// one will be created.
+@immutable
+class StatefulShellBranch {
+  /// Constructs a [StatefulShellBranch].
+  StatefulShellBranch({
+    required this.routes,
+    GlobalKey<NavigatorState>? navigatorKey,
+    this.initialLocation,
+    this.restorationScopeId,
+    this.observers,
+  }) : navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>();
+
+  /// The [GlobalKey] to be used by the [Navigator] built for this branch.
+  ///
+  /// A separate Navigator will be built for each StatefulShellBranch in a
+  /// [StatefulShellRoute] and this key will be used to identify the Navigator.
+  /// The routes associated with this branch will be placed o onto that
+  /// Navigator instead of the root Navigator.
+  final GlobalKey<NavigatorState> navigatorKey;
+
+  /// The list of child routes associated with this route branch.
+  final List<RouteBase> routes;
+
+  /// The initial location for this route branch.
+  ///
+  /// If none is specified, the location of the first descendant [GoRoute] will
+  /// be used (i.e. [defaultRoute]). The initial location is used when loading
+  /// the branch for the first time (for instance when switching branch using
+  /// the goBranch method).
+  final String? initialLocation;
+
+  /// Restoration ID to save and restore the state of the navigator, including
+  /// its history.
+  final String? restorationScopeId;
+
+  /// The observers for this branch.
+  ///
+  /// The observers parameter is used by the [Navigator] built for this branch.
+  final List<NavigatorObserver>? observers;
+
+  /// The default route of this branch, i.e. the first descendant [GoRoute].
+  ///
+  /// This route will be used when loading the branch for the first time, if
+  /// an [initialLocation] has not been provided.
+  GoRoute? get defaultRoute =>
+      RouteBase.routesRecursively(routes).whereType<GoRoute>().firstOrNull;
+}
+
+/// Builder for a custom container for the branch Navigators of a
+/// [StatefulShellRoute].
+typedef ShellNavigationContainerBuilder = Widget Function(BuildContext context,
+    StatefulNavigationShell navigationShell, List<Widget> children);
+
+/// Widget for managing the state of a [StatefulShellRoute].
+///
+/// Normally, this widget is not used directly, but is instead created
+/// internally by StatefulShellRoute. However, if a custom container for the
+/// branch Navigators is required, StatefulNavigationShell can be used in
+/// the builder or pageBuilder methods of StatefulShellRoute to facilitate this.
+/// The container is created using the provided [ShellNavigationContainerBuilder],
+/// where the List of Widgets represent the Navigators for each branch.
+///
+/// Example:
+/// ```
+/// builder: (BuildContext context, GoRouterState state,
+///     StatefulNavigationShell navigationShell) {
+///   return StatefulNavigationShell(
+///     shellRouteState: state,
+///     containerBuilder: (_, __, List<Widget> children) => MyCustomShell(shellState: state, children: children),
+///   );
+/// }
+/// ```
+class StatefulNavigationShell extends StatefulWidget {
+  /// Constructs an [StatefulNavigationShell].
+  StatefulNavigationShell({
+    required this.shellRouteContext,
+    required GoRouter router,
+    required this.containerBuilder,
+  })  : assert(shellRouteContext.route is StatefulShellRoute),
+        _router = router,
+        currentIndex = _indexOfBranchNavigatorKey(
+            shellRouteContext.route as StatefulShellRoute,
+            shellRouteContext.navigatorKey),
+        super(
+            key:
+                (shellRouteContext.route as StatefulShellRoute)._shellStateKey);
+
+  /// The ShellRouteContext responsible for building the Navigator for the
+  /// current [StatefulShellBranch].
+  final ShellRouteContext shellRouteContext;
+
+  /// The builder for a custom container for shell route Navigators.
+  final ShellNavigationContainerBuilder containerBuilder;
+
+  /// The index of the currently active [StatefulShellBranch].
+  ///
+  /// Corresponds to the index in the branches field of [StatefulShellRoute].
+  final int currentIndex;
+
+  final GoRouter _router;
+
+  /// The associated [StatefulShellRoute].
+  StatefulShellRoute get route => shellRouteContext.route as StatefulShellRoute;
+
+  /// Navigate to the last location of the [StatefulShellBranch] at the provided
+  /// index in the associated [StatefulShellBranch].
+  ///
+  /// This method will switch the currently active branch [Navigator] for the
+  /// [StatefulShellRoute]. If the branch has not been visited before, or if
+  /// initialLocation is true, this method will navigate to initial location of
+  /// the branch (see [StatefulShellBranch.initialLocation]).
+  void goBranch(int index, {bool initialLocation = false}) {
+    final StatefulShellRoute route =
+        shellRouteContext.route as StatefulShellRoute;
+    final StatefulNavigationShellState? shellState =
+        route._shellStateKey.currentState;
+    if (shellState != null) {
+      shellState.goBranch(index, initialLocation: initialLocation);
+    } else {
+      _router.go(_effectiveInitialBranchLocation(index));
+    }
+  }
+
+  /// Gets the effective initial location for the branch at the provided index
+  /// in the associated [StatefulShellRoute].
+  ///
+  /// The effective initial location is either the
+  /// [StackedShellBranch.initialLocation], if specified, or the location of the
+  /// [StackedShellBranch.defaultRoute].
+  String _effectiveInitialBranchLocation(int index) {
+    final StatefulShellRoute route =
+        shellRouteContext.route as StatefulShellRoute;
+    final StatefulShellBranch branch = route.branches[index];
+    final String? initialLocation = branch.initialLocation;
+    if (initialLocation != null) {
+      return initialLocation;
+    } else {
+      /// Recursively traverses the routes of the provided StackedShellBranch to
+      /// find the first GoRoute, from which a full path will be derived.
+      final GoRoute route = branch.defaultRoute!;
+      return _router.locationForRoute(route)!;
+    }
+  }
+
+  @override
+  State<StatefulWidget> createState() => StatefulNavigationShellState();
+
+  /// Gets the state for the nearest stateful shell route in the Widget tree.
+  static StatefulNavigationShellState of(BuildContext context) {
+    final StatefulNavigationShellState? shellState =
+        context.findAncestorStateOfType<StatefulNavigationShellState>();
+    assert(shellState != null);
+    return shellState!;
+  }
+
+  /// Gets the state for the nearest stateful shell route in the Widget tree.
+  ///
+  /// Returns null if no stateful shell route is found.
+  static StatefulNavigationShellState? maybeOf(BuildContext context) {
+    final StatefulNavigationShellState? shellState =
+        context.findAncestorStateOfType<StatefulNavigationShellState>();
+    return shellState;
+  }
+
+  static int _indexOfBranchNavigatorKey(
+      StatefulShellRoute route, GlobalKey<NavigatorState> navigatorKey) {
+    final int index = route.branches.indexWhere(
+        (StatefulShellBranch branch) => branch.navigatorKey == navigatorKey);
+    assert(index >= 0);
+    return index;
+  }
+}
+
+/// State for StatefulNavigationShell.
+class StatefulNavigationShellState extends State<StatefulNavigationShell>
+    with RestorationMixin {
+  final Map<Key, Widget> _branchNavigators = <Key, Widget>{};
+
+  /// The associated [StatefulShellRoute].
+  StatefulShellRoute get route => widget.route;
+
+  GoRouter get _router => widget._router;
+  RouteMatcher get _matcher => _router.routeInformationParser.matcher;
+
+  final Map<StatefulShellBranch, _RestorableRouteMatchList> _branchLocations =
+      <StatefulShellBranch, _RestorableRouteMatchList>{};
+
+  @override
+  String? get restorationId => route.restorationScopeId;
+
+  /// Generates a derived restoration ID for the branch location property,
+  /// falling back to the identity hash code of the branch to ensure an ID is
+  /// always returned (needed for _RestorableRouteMatchList/RestorableValue).
+  String _branchLocationRestorationScopeId(StatefulShellBranch branch) {
+    return branch.restorationScopeId != null
+        ? '${branch.restorationScopeId}-location'
+        : identityHashCode(branch).toString();
+  }
+
+  _RestorableRouteMatchList _branchLocation(StatefulShellBranch branch,
+      [bool register = true]) {
+    return _branchLocations.putIfAbsent(branch, () {
+      final _RestorableRouteMatchList branchLocation =
+          _RestorableRouteMatchList(_matcher);
+      if (register) {
+        registerForRestoration(
+            branchLocation, _branchLocationRestorationScopeId(branch));
+      }
+      return branchLocation;
+    });
+  }
+
+  RouteMatchList? _matchListForBranch(int index) =>
+      _branchLocations[route.branches[index]]?.value;
+
+  /// Creates a new RouteMatchList that is scoped to the Navigators of the
+  /// current shell route or it's descendants. This involves removing all the
+  /// trailing imperative matches from the RouterMatchList that are targeted at
+  /// any other (often top-level) Navigator.
+  RouteMatchList _scopedMatchList(RouteMatchList matchList) {
+    final Iterable<GlobalKey<NavigatorState>> validKeys =
+        route._navigatorKeysRecursively();
+    final int index = matchList.matches.indexWhere((RouteMatch e) {
+      final RouteBase route = e.route;
+      if (e is ImperativeRouteMatch && route is GoRoute) {
+        return route.parentNavigatorKey != null &&
+            !validKeys.contains(route.parentNavigatorKey);
+      }
+      return false;
+    });
+    if (index > 0) {
+      final List<RouteMatch> matches = matchList.matches.sublist(0, index);
+      return RouteMatchList(
+        matches: matches,
+        uri: Uri.parse(matches.last.matchedLocation),
+        pathParameters: matchList.pathParameters,
+      );
+    }
+    return matchList;
+  }
+
+  void _updateCurrentBranchStateFromWidget() {
+    final StatefulShellBranch branch = route.branches[widget.currentIndex];
+    final ShellRouteContext shellRouteContext = widget.shellRouteContext;
+    final RouteMatchList currentBranchLocation =
+        _scopedMatchList(shellRouteContext.routeMatchList);
+
+    final _RestorableRouteMatchList branchLocation =
+        _branchLocation(branch, false);
+    final RouteMatchList previousBranchLocation = branchLocation.value;
+    branchLocation.value = currentBranchLocation;
+    final bool hasExistingNavigator =
+        _branchNavigators[branch.navigatorKey] != null;
+
+    /// Only update the Navigator of the route match list has changed
+    final bool locationChanged =
+        previousBranchLocation != currentBranchLocation;
+    if (locationChanged || !hasExistingNavigator) {
+      _branchNavigators[branch.navigatorKey] = shellRouteContext
+          .navigatorBuilder(branch.observers, branch.restorationScopeId);
+    }
+  }
+
+  /// The index of the currently active [StatefulShellBranch].
+  ///
+  /// Corresponds to the index in the branches field of [StatefulShellRoute].
+  int get currentIndex => widget.currentIndex;
+
+  /// Navigate to the last location of the [StatefulShellBranch] at the provided
+  /// index in the associated [StatefulShellBranch].
+  ///
+  /// This method will switch the currently active branch [Navigator] for the
+  /// [StatefulShellRoute]. If the branch has not been visited before, or if
+  /// initialLocation is true, this method will navigate to initial location of
+  /// the branch (see [StatefulShellBranch.initialLocation]).
+  void goBranch(int index, {bool initialLocation = false}) {
+    assert(index >= 0 && index < route.branches.length);
+    final RouteMatchList? matchlist =
+        initialLocation ? null : _matchListForBranch(index);
+    if (matchlist != null && matchlist.isNotEmpty) {
+      final RouteInformation preParsed =
+          matchlist.toPreParsedRouteInformation();
+      // TODO(tolo): remove this ignore and migrate the code
+      // https://github.com/flutter/flutter/issues/124045.
+      // ignore: deprecated_member_use, unnecessary_non_null_assertion
+      _router.go(preParsed.location!, extra: preParsed.state);
+    } else {
+      _router.go(widget._effectiveInitialBranchLocation(index));
+    }
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _updateCurrentBranchStateFromWidget();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    for (final StatefulShellBranch branch in route.branches) {
+      _branchLocations[branch]?.dispose();
+    }
+  }
+
+  @override
+  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
+    route.branches.forEach(_branchLocation);
+  }
+
+  @override
+  void didUpdateWidget(covariant StatefulNavigationShell oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    _updateCurrentBranchStateFromWidget();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> children = route.branches
+        .map((StatefulShellBranch branch) => _BranchNavigatorProxy(
+            key: ObjectKey(branch),
+            branch: branch,
+            navigatorForBranch: (StatefulShellBranch b) =>
+                _branchNavigators[b.navigatorKey]))
+        .toList();
+
+    return widget.containerBuilder(context, widget, children);
+  }
+}
+
+/// [RestorableProperty] for enabling state restoration of [RouteMatchList]s.
+class _RestorableRouteMatchList extends RestorableProperty<RouteMatchList> {
+  _RestorableRouteMatchList(RouteMatcher matcher)
+      : _matchListCodec = RouteMatchListCodec(matcher);
+
+  final RouteMatchListCodec _matchListCodec;
+
+  RouteMatchList get value => _value;
+  RouteMatchList _value = RouteMatchList.empty;
+  set value(RouteMatchList newValue) {
+    if (newValue != _value) {
+      _value = newValue;
+      notifyListeners();
+    }
+  }
+
+  @override
+  void initWithValue(RouteMatchList value) {
+    _value = value;
+  }
+
+  @override
+  RouteMatchList createDefaultValue() => RouteMatchList.empty;
+
+  @override
+  RouteMatchList fromPrimitives(Object? data) {
+    return _matchListCodec.decodeMatchList(data) ?? RouteMatchList.empty;
+  }
+
+  @override
+  Object? toPrimitives() {
+    if (value != null && value.isNotEmpty) {
+      return _matchListCodec.encodeMatchList(value);
+    }
+    return null;
+  }
+}
+
+typedef _NavigatorForBranch = Widget? Function(StatefulShellBranch);
+
+/// Widget that serves as the proxy for a branch Navigator Widget, which
+/// possibly hasn't been created yet.
+///
+/// This Widget hides the logic handling whether a Navigator Widget has been
+/// created yet for a branch or not, and at the same time ensures that the same
+/// Widget class is consistently passed to the containerBuilder. The latter is
+/// important for container implementations that cache child widgets,
+/// such as [TabBarView].
+class _BranchNavigatorProxy extends StatefulWidget {
+  const _BranchNavigatorProxy({
+    super.key,
+    required this.branch,
+    required this.navigatorForBranch,
+  });
+
+  final StatefulShellBranch branch;
+  final _NavigatorForBranch navigatorForBranch;
+
+  @override
+  State<StatefulWidget> createState() => _BranchNavigatorProxyState();
+}
+
+/// State for _BranchNavigatorProxy, using AutomaticKeepAliveClientMixin to
+/// properly handle some scenarios where Slivers are used to manage the branches
+/// (such as [TabBarView]).
+class _BranchNavigatorProxyState extends State<_BranchNavigatorProxy>
+    with AutomaticKeepAliveClientMixin {
+  @override
+  Widget build(BuildContext context) {
+    super.build(context);
+    return widget.navigatorForBranch(widget.branch) ?? const SizedBox.shrink();
+  }
+
+  @override
+  bool get wantKeepAlive => true;
+}
+
+/// Default implementation of a container widget for the [Navigator]s of the
+/// route branches. This implementation uses an [IndexedStack] as a container.
+class _IndexedStackedRouteBranchContainer extends StatelessWidget {
+  const _IndexedStackedRouteBranchContainer(
+      {required this.currentIndex, required this.children});
+
+  final int currentIndex;
+
+  final List<Widget> children;
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> stackItems = children
+        .mapIndexed((int index, Widget child) =>
+            _buildRouteBranchContainer(context, currentIndex == index, child))
+        .toList();
+
+    return IndexedStack(index: currentIndex, children: stackItems);
+  }
+
+  Widget _buildRouteBranchContainer(
+      BuildContext context, bool isActive, Widget child) {
+    return Offstage(
+      offstage: !isActive,
+      child: TickerMode(
+        enabled: isActive,
+        child: child,
+      ),
+    );
+  }
 }
diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart
index e9b4ec6..0e2d59b 100644
--- a/packages/go_router/lib/src/router.dart
+++ b/packages/go_router/lib/src/router.dart
@@ -143,11 +143,6 @@
   GoRouteInformationParser get routeInformationParser =>
       _routeInformationParser;
 
-  /// The route configuration. Used for testing.
-  // TODO(johnpryan): Remove this, integration tests shouldn't need access
-  @visibleForTesting
-  RouteConfiguration get routeConfiguration => _routeConfiguration;
-
   /// Gets the current location.
   // TODO(chunhtai): deprecates this once go_router_builder is migrated to
   // GoRouterState.of.
@@ -189,6 +184,13 @@
         queryParameters: queryParameters,
       );
 
+  /// Get the location for the provided route.
+  ///
+  /// Builds the absolute path for the route, by concatenating the paths of the
+  /// route and all its ancestors.
+  String? locationForRoute(RouteBase route) =>
+      _routeInformationParser.configuration.locationForRoute(route);
+
   /// Navigate to a URI location w/ optional query parameters, e.g.
   /// `/family/f2/person/p1?color=blue`
   void go(String location, {Object? extra}) {
diff --git a/packages/go_router/lib/src/typedefs.dart b/packages/go_router/lib/src/typedefs.dart
index 2926994..7a40715 100644
--- a/packages/go_router/lib/src/typedefs.dart
+++ b/packages/go_router/lib/src/typedefs.dart
@@ -34,11 +34,18 @@
   Widget child,
 );
 
-/// The signature of the navigatorBuilder callback.
-typedef GoRouterNavigatorBuilder = Widget Function(
+/// The widget builder for [StatefulShellRoute].
+typedef StatefulShellRouteBuilder = Widget Function(
   BuildContext context,
   GoRouterState state,
-  Widget child,
+  StatefulNavigationShell navigationShell,
+);
+
+/// The page builder for [StatefulShellRoute].
+typedef StatefulShellRoutePageBuilder = Page<dynamic> Function(
+  BuildContext context,
+  GoRouterState state,
+  StatefulNavigationShell navigationShell,
 );
 
 /// Signature of a go router builder function with navigator.
@@ -50,3 +57,7 @@
 /// The signature of the redirect callback.
 typedef GoRouterRedirect = FutureOr<String?> Function(
     BuildContext context, GoRouterState state);
+
+/// Signature for functions used to build Navigators
+typedef NavigatorBuilder = Widget Function(
+    List<NavigatorObserver>? observers, String? restorationScopeId);
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index 53c28f8..00f849d 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: 7.0.2
+version: 7.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/builder_test.dart b/packages/go_router/test/builder_test.dart
index 81f4182..ac97cab 100644
--- a/packages/go_router/test/builder_test.dart
+++ b/packages/go_router/test/builder_test.dart
@@ -8,6 +8,9 @@
 import 'package:go_router/src/configuration.dart';
 import 'package:go_router/src/match.dart';
 import 'package:go_router/src/matching.dart';
+import 'package:go_router/src/router.dart';
+
+import 'test_helpers.dart';
 
 void main() {
   group('RouteBuilder', () {
@@ -76,17 +79,13 @@
       );
 
       final RouteMatchList matches = RouteMatchList(
-          matches: <RouteMatch>[
-            RouteMatch(
-              route: config.routes.first,
-              matchedLocation: '/',
-              extra: null,
-              error: null,
-              pageKey: const ValueKey<String>('/'),
-            ),
-          ],
-          uri: Uri.parse('/'),
-          pathParameters: const <String, String>{});
+        matches: <RouteMatch>[
+          createRouteMatch(config.routes.first, '/'),
+          createRouteMatch(config.routes.first.routes.first, '/'),
+        ],
+        uri: Uri.parse('/'),
+        pathParameters: const <String, String>{},
+      );
 
       await tester.pumpWidget(
         _BuilderTestWidget(
@@ -272,6 +271,104 @@
       expect(find.byType(_HomeScreen), findsNothing);
       expect(find.byType(_DetailsScreen), findsOneWidget);
     });
+
+    testWidgets('Uses the correct restorationScopeId for ShellRoute',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> shellNavigatorKey =
+          GlobalKey<NavigatorState>(debugLabel: 'shell');
+      final RouteConfiguration config = RouteConfiguration(
+        navigatorKey: rootNavigatorKey,
+        routes: <RouteBase>[
+          ShellRoute(
+            builder: (BuildContext context, GoRouterState state, Widget child) {
+              return _HomeScreen(child: child);
+            },
+            navigatorKey: shellNavigatorKey,
+            restorationScopeId: 'scope1',
+            routes: <RouteBase>[
+              GoRoute(
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) {
+                  return _DetailsScreen();
+                },
+              ),
+            ],
+          ),
+        ],
+        redirectLimit: 10,
+        topRedirect: (BuildContext context, GoRouterState state) {
+          return null;
+        },
+      );
+
+      final RouteMatchList matches = RouteMatchList(
+        matches: <RouteMatch>[
+          createRouteMatch(config.routes.first, ''),
+          createRouteMatch(config.routes.first.routes.first, '/a'),
+        ],
+        uri: Uri.parse('/b'),
+        pathParameters: const <String, String>{},
+      );
+
+      await tester.pumpWidget(
+        _BuilderTestWidget(
+          routeConfiguration: config,
+          matches: matches,
+        ),
+      );
+
+      expect(find.byKey(rootNavigatorKey), findsOneWidget);
+      expect(find.byKey(shellNavigatorKey), findsOneWidget);
+      expect(
+          (shellNavigatorKey.currentWidget as Navigator?)?.restorationScopeId,
+          'scope1');
+    });
+
+    testWidgets('Uses the correct restorationScopeId for StatefulShellRoute',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> shellNavigatorKey =
+          GlobalKey<NavigatorState>(debugLabel: 'shell');
+      final GoRouter goRouter = GoRouter(
+        initialLocation: '/a',
+        navigatorKey: rootNavigatorKey,
+        routes: <RouteBase>[
+          StatefulShellRoute.indexedStack(
+            restorationScopeId: 'shell',
+            builder: (BuildContext context, GoRouterState state,
+                    StatefulNavigationShell navigationShell) =>
+                _HomeScreen(child: navigationShell),
+            branches: <StatefulShellBranch>[
+              StatefulShellBranch(
+                navigatorKey: shellNavigatorKey,
+                restorationScopeId: 'scope1',
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: '/a',
+                    builder: (BuildContext context, GoRouterState state) {
+                      return _DetailsScreen();
+                    },
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ],
+      );
+
+      await tester.pumpWidget(MaterialApp.router(
+        routerConfig: goRouter,
+      ));
+
+      expect(find.byKey(rootNavigatorKey), findsOneWidget);
+      expect(find.byKey(shellNavigatorKey), findsOneWidget);
+      expect(
+          (shellNavigatorKey.currentWidget as Navigator?)?.restorationScopeId,
+          'scope1');
+    });
   });
 }
 
@@ -340,15 +437,15 @@
       },
       restorationScopeId: null,
       observers: <NavigatorObserver>[],
+      onPopPageWithRouteMatch: (_, __, ___) => false,
     );
   }
 
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
-      home: builder.tryBuild(context, matches, (_, __) => false, false,
+      home: builder.tryBuild(context, matches, false,
           routeConfiguration.navigatorKey, <Page<Object?>, GoRouterState>{}),
-      // builder: (context, child) => ,
     );
   }
 }
diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart
index 08cf53c..3124702 100644
--- a/packages/go_router/test/configuration_test.dart
+++ b/packages/go_router/test/configuration_test.dart
@@ -6,6 +6,8 @@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/src/configuration.dart';
 
+import 'test_helpers.dart';
+
 void main() {
   group('RouteConfiguration', () {
     test('throws when parentNavigatorKey is not an ancestor', () {
@@ -81,6 +83,483 @@
     });
 
     test(
+        'throws when StatefulShellRoute sub-route uses incorrect parentNavigatorKey',
+        () {
+      final GlobalKey<NavigatorState> root =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> keyA =
+          GlobalKey<NavigatorState>(debugLabel: 'A');
+      final GlobalKey<NavigatorState> keyB =
+          GlobalKey<NavigatorState>(debugLabel: 'B');
+
+      expect(
+        () {
+          RouteConfiguration(
+            navigatorKey: root,
+            routes: <RouteBase>[
+              StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+                StatefulShellBranch(
+                  navigatorKey: keyA,
+                  routes: <RouteBase>[
+                    GoRoute(
+                        path: '/a',
+                        builder: _mockScreenBuilder,
+                        routes: <RouteBase>[
+                          GoRoute(
+                              path: 'details',
+                              builder: _mockScreenBuilder,
+                              parentNavigatorKey: keyB),
+                        ]),
+                  ],
+                ),
+              ], builder: mockStackedShellBuilder),
+            ],
+            redirectLimit: 10,
+            topRedirect: (BuildContext context, GoRouterState state) {
+              return null;
+            },
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    test(
+        'does not throw when StatefulShellRoute sub-route uses correct parentNavigatorKeys',
+        () {
+      final GlobalKey<NavigatorState> root =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> keyA =
+          GlobalKey<NavigatorState>(debugLabel: 'A');
+
+      RouteConfiguration(
+        navigatorKey: root,
+        routes: <RouteBase>[
+          StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+            StatefulShellBranch(
+              navigatorKey: keyA,
+              routes: <RouteBase>[
+                GoRoute(
+                    path: '/a',
+                    builder: _mockScreenBuilder,
+                    routes: <RouteBase>[
+                      GoRoute(
+                          path: 'details',
+                          builder: _mockScreenBuilder,
+                          parentNavigatorKey: keyA),
+                    ]),
+              ],
+            ),
+          ], builder: mockStackedShellBuilder),
+        ],
+        redirectLimit: 10,
+        topRedirect: (BuildContext context, GoRouterState state) {
+          return null;
+        },
+      );
+    });
+
+    test(
+        'throws when a sub-route of StatefulShellRoute has a parentNavigatorKey',
+        () {
+      final GlobalKey<NavigatorState> root =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> someNavigatorKey =
+          GlobalKey<NavigatorState>();
+      expect(
+        () {
+          RouteConfiguration(
+            navigatorKey: root,
+            routes: <RouteBase>[
+              StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+                StatefulShellBranch(
+                  routes: <RouteBase>[
+                    GoRoute(
+                        path: '/a',
+                        builder: _mockScreenBuilder,
+                        routes: <RouteBase>[
+                          GoRoute(
+                              path: 'details',
+                              builder: _mockScreenBuilder,
+                              parentNavigatorKey: someNavigatorKey),
+                        ]),
+                  ],
+                ),
+                StatefulShellBranch(
+                  routes: <RouteBase>[
+                    GoRoute(
+                        path: '/b',
+                        builder: _mockScreenBuilder,
+                        parentNavigatorKey: someNavigatorKey),
+                  ],
+                ),
+              ], builder: mockStackedShellBuilder),
+            ],
+            redirectLimit: 10,
+            topRedirect: (BuildContext context, GoRouterState state) {
+              return null;
+            },
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    test('throws when StatefulShellRoute has duplicate navigator keys', () {
+      final GlobalKey<NavigatorState> root =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> keyA =
+          GlobalKey<NavigatorState>(debugLabel: 'A');
+      final List<GoRoute> shellRouteChildren = <GoRoute>[
+        GoRoute(
+            path: '/a', builder: _mockScreenBuilder, parentNavigatorKey: keyA),
+        GoRoute(
+            path: '/b', builder: _mockScreenBuilder, parentNavigatorKey: keyA),
+      ];
+      expect(
+        () {
+          RouteConfiguration(
+            navigatorKey: root,
+            routes: <RouteBase>[
+              StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+                StatefulShellBranch(routes: shellRouteChildren)
+              ], builder: mockStackedShellBuilder),
+            ],
+            redirectLimit: 10,
+            topRedirect: (BuildContext context, GoRouterState state) {
+              return null;
+            },
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    test(
+        'throws when a child of StatefulShellRoute has an incorrect '
+        'parentNavigatorKey', () {
+      final GlobalKey<NavigatorState> root =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> sectionANavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<NavigatorState> sectionBNavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GoRoute routeA = GoRoute(
+          path: '/a',
+          builder: _mockScreenBuilder,
+          parentNavigatorKey: sectionBNavigatorKey);
+      final GoRoute routeB = GoRoute(
+          path: '/b',
+          builder: _mockScreenBuilder,
+          parentNavigatorKey: sectionANavigatorKey);
+      expect(
+        () {
+          RouteConfiguration(
+            navigatorKey: root,
+            routes: <RouteBase>[
+              StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+                StatefulShellBranch(
+                    routes: <RouteBase>[routeA],
+                    navigatorKey: sectionANavigatorKey),
+                StatefulShellBranch(
+                    routes: <RouteBase>[routeB],
+                    navigatorKey: sectionBNavigatorKey),
+              ], builder: mockStackedShellBuilder),
+            ],
+            redirectLimit: 10,
+            topRedirect: (BuildContext context, GoRouterState state) {
+              return null;
+            },
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    test(
+        'throws when a branch of a StatefulShellRoute has an incorrect '
+        'initialLocation', () {
+      final GlobalKey<NavigatorState> root =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> sectionANavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<NavigatorState> sectionBNavigatorKey =
+          GlobalKey<NavigatorState>();
+      expect(
+        () {
+          RouteConfiguration(
+            navigatorKey: root,
+            routes: <RouteBase>[
+              StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+                StatefulShellBranch(
+                  initialLocation: '/x',
+                  navigatorKey: sectionANavigatorKey,
+                  routes: <RouteBase>[
+                    GoRoute(
+                      path: '/a',
+                      builder: _mockScreenBuilder,
+                    ),
+                  ],
+                ),
+                StatefulShellBranch(
+                  navigatorKey: sectionBNavigatorKey,
+                  routes: <RouteBase>[
+                    GoRoute(
+                      path: '/b',
+                      builder: _mockScreenBuilder,
+                    ),
+                  ],
+                ),
+              ], builder: mockStackedShellBuilder),
+            ],
+            redirectLimit: 10,
+            topRedirect: (BuildContext context, GoRouterState state) {
+              return null;
+            },
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    test(
+        'throws when a branch of a StatefulShellRoute has a initialLocation '
+        'that is not a descendant of the same branch', () {
+      final GlobalKey<NavigatorState> root =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+      final GlobalKey<NavigatorState> sectionANavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<NavigatorState> sectionBNavigatorKey =
+          GlobalKey<NavigatorState>();
+      expect(
+        () {
+          RouteConfiguration(
+            navigatorKey: root,
+            routes: <RouteBase>[
+              StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+                StatefulShellBranch(
+                  initialLocation: '/b',
+                  navigatorKey: sectionANavigatorKey,
+                  routes: <RouteBase>[
+                    GoRoute(
+                      path: '/a',
+                      builder: _mockScreenBuilder,
+                    ),
+                  ],
+                ),
+                StatefulShellBranch(
+                  initialLocation: '/b',
+                  navigatorKey: sectionBNavigatorKey,
+                  routes: <RouteBase>[
+                    StatefulShellRoute.indexedStack(
+                        branches: <StatefulShellBranch>[
+                          StatefulShellBranch(
+                            routes: <RouteBase>[
+                              GoRoute(
+                                path: '/b',
+                                builder: _mockScreenBuilder,
+                              ),
+                            ],
+                          ),
+                        ],
+                        builder: mockStackedShellBuilder),
+                  ],
+                ),
+              ], builder: mockStackedShellBuilder),
+            ],
+            redirectLimit: 10,
+            topRedirect: (BuildContext context, GoRouterState state) {
+              return null;
+            },
+          );
+        },
+        throwsAssertionError,
+      );
+    });
+
+    test(
+        'does not throw when a branch of a StatefulShellRoute has correctly '
+        'configured initialLocations', () {
+      final GlobalKey<NavigatorState> root =
+          GlobalKey<NavigatorState>(debugLabel: 'root');
+
+      RouteConfiguration(
+        navigatorKey: root,
+        routes: <RouteBase>[
+          StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+            StatefulShellBranch(
+              routes: <RouteBase>[
+                GoRoute(
+                    path: '/a',
+                    builder: _mockScreenBuilder,
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'detail',
+                        builder: _mockScreenBuilder,
+                      ),
+                    ]),
+              ],
+            ),
+            StatefulShellBranch(
+              initialLocation: '/b/detail',
+              routes: <RouteBase>[
+                GoRoute(
+                    path: '/b',
+                    builder: _mockScreenBuilder,
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'detail',
+                        builder: _mockScreenBuilder,
+                      ),
+                    ]),
+              ],
+            ),
+            StatefulShellBranch(
+              initialLocation: '/c/detail',
+              routes: <RouteBase>[
+                StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+                  StatefulShellBranch(
+                    routes: <RouteBase>[
+                      GoRoute(
+                          path: '/c',
+                          builder: _mockScreenBuilder,
+                          routes: <RouteBase>[
+                            GoRoute(
+                              path: 'detail',
+                              builder: _mockScreenBuilder,
+                            ),
+                          ]),
+                    ],
+                  ),
+                  StatefulShellBranch(
+                    initialLocation: '/d/detail',
+                    routes: <RouteBase>[
+                      GoRoute(
+                          path: '/d',
+                          builder: _mockScreenBuilder,
+                          routes: <RouteBase>[
+                            GoRoute(
+                              path: 'detail',
+                              builder: _mockScreenBuilder,
+                            ),
+                          ]),
+                    ],
+                  ),
+                ], builder: mockStackedShellBuilder),
+              ],
+            ),
+            StatefulShellBranch(routes: <RouteBase>[
+              ShellRoute(
+                builder: _mockShellBuilder,
+                routes: <RouteBase>[
+                  ShellRoute(
+                    builder: _mockShellBuilder,
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: '/e',
+                        builder: _mockScreenBuilder,
+                      ),
+                    ],
+                  )
+                ],
+              ),
+            ]),
+          ], builder: mockStackedShellBuilder),
+        ],
+        redirectLimit: 10,
+        topRedirect: (BuildContext context, GoRouterState state) {
+          return null;
+        },
+      );
+    });
+
+    test(
+      'derives the correct initialLocation for a StatefulShellBranch',
+      () {
+        final StatefulShellBranch branchA;
+        final StatefulShellBranch branchY;
+        final StatefulShellBranch branchB;
+
+        final RouteConfiguration config = RouteConfiguration(
+          navigatorKey: GlobalKey<NavigatorState>(debugLabel: 'root'),
+          routes: <RouteBase>[
+            StatefulShellRoute.indexedStack(
+              builder: mockStackedShellBuilder,
+              branches: <StatefulShellBranch>[
+                branchA = StatefulShellBranch(routes: <RouteBase>[
+                  GoRoute(
+                    path: '/a',
+                    builder: _mockScreenBuilder,
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'x',
+                        builder: _mockScreenBuilder,
+                        routes: <RouteBase>[
+                          StatefulShellRoute.indexedStack(
+                              builder: mockStackedShellBuilder,
+                              branches: <StatefulShellBranch>[
+                                branchY =
+                                    StatefulShellBranch(routes: <RouteBase>[
+                                  ShellRoute(
+                                      builder: _mockShellBuilder,
+                                      routes: <RouteBase>[
+                                        GoRoute(
+                                          path: 'y1',
+                                          builder: _mockScreenBuilder,
+                                        ),
+                                        GoRoute(
+                                          path: 'y2',
+                                          builder: _mockScreenBuilder,
+                                        ),
+                                      ])
+                                ])
+                              ]),
+                        ],
+                      ),
+                    ],
+                  ),
+                ]),
+                branchB = StatefulShellBranch(routes: <RouteBase>[
+                  ShellRoute(
+                    builder: _mockShellBuilder,
+                    routes: <RouteBase>[
+                      ShellRoute(
+                        builder: _mockShellBuilder,
+                        routes: <RouteBase>[
+                          GoRoute(
+                            path: '/b1',
+                            builder: _mockScreenBuilder,
+                          ),
+                          GoRoute(
+                            path: '/b2',
+                            builder: _mockScreenBuilder,
+                          ),
+                        ],
+                      )
+                    ],
+                  ),
+                ]),
+              ],
+            ),
+          ],
+          redirectLimit: 10,
+          topRedirect: (BuildContext context, GoRouterState state) {
+            return null;
+          },
+        );
+
+        String? initialLocation(StatefulShellBranch branch) {
+          final GoRoute? route = branch.defaultRoute;
+          return route != null ? config.locationForRoute(route) : null;
+        }
+
+        expect('/a', initialLocation(branchA));
+        expect('/a/x/y1', initialLocation(branchY));
+        expect('/b1', initialLocation(branchB));
+      },
+    );
+
+    test(
         'throws when there is a GoRoute ancestor with a different parentNavigatorKey',
         () {
       final GlobalKey<NavigatorState> root =
diff --git a/packages/go_router/test/delegate_test.dart b/packages/go_router/test/delegate_test.dart
index a0533a1..c25877b 100644
--- a/packages/go_router/test/delegate_test.dart
+++ b/packages/go_router/test/delegate_test.dart
@@ -8,6 +8,8 @@
 import 'package:go_router/src/match.dart';
 import 'package:go_router/src/misc/error_screen.dart';
 
+import 'test_helpers.dart';
+
 Future<GoRouter> createGoRouter(
   WidgetTester tester, {
   Listenable? refreshListenable,
@@ -30,6 +32,46 @@
   return router;
 }
 
+Future<GoRouter> createGoRouterWithStatefulShellRoute(
+    WidgetTester tester) async {
+  final GoRouter router = GoRouter(
+    initialLocation: '/',
+    routes: <RouteBase>[
+      GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
+      GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()),
+      StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
+        StatefulShellBranch(routes: <RouteBase>[
+          GoRoute(
+              path: '/c',
+              builder: (_, __) => const DummyStatefulWidget(),
+              routes: <RouteBase>[
+                GoRoute(
+                    path: 'c1',
+                    builder: (_, __) => const DummyStatefulWidget()),
+                GoRoute(
+                    path: 'c2',
+                    builder: (_, __) => const DummyStatefulWidget()),
+              ]),
+        ]),
+        StatefulShellBranch(routes: <RouteBase>[
+          GoRoute(
+              path: '/d',
+              builder: (_, __) => const DummyStatefulWidget(),
+              routes: <RouteBase>[
+                GoRoute(
+                    path: 'd1',
+                    builder: (_, __) => const DummyStatefulWidget()),
+              ]),
+        ]),
+      ], builder: mockStackedShellBuilder),
+    ],
+  );
+  await tester.pumpWidget(MaterialApp.router(
+    routerConfig: router,
+  ));
+  return router;
+}
+
 void main() {
   group('pop', () {
     testWidgets('removes the last element', (WidgetTester tester) async {
@@ -79,6 +121,66 @@
         );
       },
     );
+
+    testWidgets(
+      'It should successfully push a route from outside the the current '
+      'StatefulShellRoute',
+      (WidgetTester tester) async {
+        final GoRouter goRouter =
+            await createGoRouterWithStatefulShellRoute(tester);
+        goRouter.push('/c/c1');
+        await tester.pumpAndSettle();
+
+        goRouter.push('/a');
+        await tester.pumpAndSettle();
+
+        expect(goRouter.routerDelegate.matches.matches.length, 3);
+        expect(
+          goRouter.routerDelegate.matches.matches[2].pageKey,
+          const Key('/a-p0'),
+        );
+      },
+    );
+
+    testWidgets(
+      'It should successfully push a route that is a descendant of the current '
+      'StatefulShellRoute branch',
+      (WidgetTester tester) async {
+        final GoRouter goRouter =
+            await createGoRouterWithStatefulShellRoute(tester);
+        goRouter.push('/c/c1');
+        await tester.pumpAndSettle();
+
+        goRouter.push('/c/c2');
+        await tester.pumpAndSettle();
+
+        expect(goRouter.routerDelegate.matches.matches.length, 3);
+        expect(
+          goRouter.routerDelegate.matches.matches[2].pageKey,
+          const Key('/c/c2-p0'),
+        );
+      },
+    );
+
+    testWidgets(
+      'It should successfully push the root of the current StatefulShellRoute '
+      'branch upon itself',
+      (WidgetTester tester) async {
+        final GoRouter goRouter =
+            await createGoRouterWithStatefulShellRoute(tester);
+        goRouter.push('/c');
+        await tester.pumpAndSettle();
+
+        goRouter.push('/c');
+        await tester.pumpAndSettle();
+
+        expect(goRouter.routerDelegate.matches.matches.length, 3);
+        expect(
+          goRouter.routerDelegate.matches.matches[2].pageKey,
+          const Key('/c-p1'),
+        );
+      },
+    );
   });
 
   group('canPop', () {
diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart
index a93b492..532a4f6 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -2504,6 +2504,87 @@
       expect(imperativeRouteMatch.matches.pathParameters['pid'], pid);
     });
 
+    testWidgets('StatefulShellRoute supports nested routes with params',
+        (WidgetTester tester) async {
+      StatefulNavigationShell? routeState;
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(
+              routes: <RouteBase>[
+                GoRoute(
+                  path: '/a',
+                  builder: (BuildContext context, GoRouterState state) =>
+                      const Text('Screen A'),
+                ),
+              ],
+            ),
+            StatefulShellBranch(
+              routes: <RouteBase>[
+                GoRoute(
+                    path: '/family',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const Text('Families'),
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: ':fid',
+                        builder: (BuildContext context, GoRouterState state) =>
+                            FamilyScreen(state.pathParameters['fid']!),
+                        routes: <GoRoute>[
+                          GoRoute(
+                            path: 'person/:pid',
+                            builder:
+                                (BuildContext context, GoRouterState state) {
+                              final String fid = state.pathParameters['fid']!;
+                              final String pid = state.pathParameters['pid']!;
+
+                              return PersonScreen(fid, pid);
+                            },
+                          ),
+                        ],
+                      )
+                    ]),
+              ],
+            ),
+          ],
+        ),
+      ];
+
+      final GoRouter router =
+          await createRouter(routes, tester, initialLocation: '/a');
+      const String fid = 'f1';
+      const String pid = 'p2';
+      const String loc = '/family/$fid/person/$pid';
+
+      router.go(loc);
+      await tester.pumpAndSettle();
+      RouteMatchList matches = router.routerDelegate.matches;
+
+      expect(router.location, loc);
+      expect(matches.matches, hasLength(4));
+      expect(find.byType(PersonScreen), findsOneWidget);
+      expect(matches.pathParameters['fid'], fid);
+      expect(matches.pathParameters['pid'], pid);
+
+      routeState?.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsOneWidget);
+      expect(find.byType(PersonScreen), findsNothing);
+
+      routeState?.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.byType(PersonScreen), findsOneWidget);
+      matches = router.routerDelegate.matches;
+      expect(matches.pathParameters['fid'], fid);
+      expect(matches.pathParameters['pid'], pid);
+    });
+
     testWidgets('goNames should allow dynamics values for queryParams',
         (WidgetTester tester) async {
       const Map<String, dynamic> queryParametersAll = <String, List<dynamic>>{
@@ -2993,6 +3074,766 @@
       expect(find.text('Screen B'), findsOneWidget);
       expect(find.text('Screen C'), findsNothing);
     });
+
+    testWidgets('Builds StatefulShellRoute', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+                  StatefulNavigationShell navigationShell) =>
+              navigationShell,
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen A'),
+              ),
+            ]),
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/b',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen B'),
+              ),
+            ]),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/a', navigatorKey: rootNavigatorKey);
+      expect(find.text('Screen A'), findsOneWidget);
+      expect(find.text('Screen B'), findsNothing);
+
+      router.go('/b');
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B'), findsOneWidget);
+    });
+
+    testWidgets('Builds StatefulShellRoute as a sub-route',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+
+      final List<RouteBase> routes = <RouteBase>[
+        GoRoute(
+          path: '/root',
+          builder: (BuildContext context, GoRouterState state) =>
+              const Text('Root'),
+          routes: <RouteBase>[
+            StatefulShellRoute.indexedStack(
+              builder: (BuildContext context, GoRouterState state,
+                      StatefulNavigationShell navigationShell) =>
+                  navigationShell,
+              branches: <StatefulShellBranch>[
+                StatefulShellBranch(routes: <GoRoute>[
+                  GoRoute(
+                    path: 'a',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const Text('Screen A'),
+                  ),
+                ]),
+                StatefulShellBranch(routes: <GoRoute>[
+                  GoRoute(
+                    path: 'b',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const Text('Screen B'),
+                  ),
+                ]),
+              ],
+            ),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/root/a', navigatorKey: rootNavigatorKey);
+      expect(find.text('Screen A'), findsOneWidget);
+      expect(find.text('Screen B'), findsNothing);
+
+      router.go('/root/b');
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B'), findsOneWidget);
+    });
+
+    testWidgets(
+        'Navigation with goBranch is correctly handled in StatefulShellRoute',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<DummyStatefulWidgetState> statefulWidgetKey =
+          GlobalKey<DummyStatefulWidgetState>();
+      StatefulNavigationShell? routeState;
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(
+              routes: <RouteBase>[
+                GoRoute(
+                  path: '/a',
+                  builder: (BuildContext context, GoRouterState state) =>
+                      const Text('Screen A'),
+                ),
+              ],
+            ),
+            StatefulShellBranch(
+              routes: <RouteBase>[
+                GoRoute(
+                  path: '/b',
+                  builder: (BuildContext context, GoRouterState state) =>
+                      const Text('Screen B'),
+                ),
+              ],
+            ),
+            StatefulShellBranch(
+              routes: <RouteBase>[
+                GoRoute(
+                  path: '/c',
+                  builder: (BuildContext context, GoRouterState state) =>
+                      const Text('Screen C'),
+                ),
+              ],
+            ),
+            StatefulShellBranch(
+              routes: <RouteBase>[
+                GoRoute(
+                  path: '/d',
+                  builder: (BuildContext context, GoRouterState state) =>
+                      const Text('Screen D'),
+                ),
+              ],
+            ),
+          ],
+        ),
+      ];
+
+      await createRouter(routes, tester,
+          initialLocation: '/a', navigatorKey: rootNavigatorKey);
+      statefulWidgetKey.currentState?.increment();
+      expect(find.text('Screen A'), findsOneWidget);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Screen C'), findsNothing);
+      expect(find.text('Screen D'), findsNothing);
+
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B'), findsOneWidget);
+      expect(find.text('Screen C'), findsNothing);
+      expect(find.text('Screen D'), findsNothing);
+
+      routeState!.goBranch(2);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Screen C'), findsOneWidget);
+      expect(find.text('Screen D'), findsNothing);
+
+      routeState!.goBranch(3);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Screen C'), findsNothing);
+      expect(find.text('Screen D'), findsOneWidget);
+
+      expect(() {
+        // Verify that navigation to unknown index fails
+        routeState!.goBranch(4);
+      }, throwsA(isA<Error>()));
+    });
+
+    testWidgets(
+        'Navigates to correct nested navigation tree in StatefulShellRoute '
+        'and maintains state', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<DummyStatefulWidgetState> statefulWidgetKey =
+          GlobalKey<DummyStatefulWidgetState>();
+      StatefulNavigationShell? routeState;
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen A'),
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: 'detailA',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        Column(children: <Widget>[
+                      const Text('Screen A Detail'),
+                      DummyStatefulWidget(key: statefulWidgetKey),
+                    ]),
+                  ),
+                ],
+              ),
+            ]),
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/b',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen B'),
+              ),
+            ]),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey);
+      statefulWidgetKey.currentState?.increment();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen A Detail'), findsOneWidget);
+      expect(find.text('Screen B'), findsNothing);
+
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen A Detail'), findsNothing);
+      expect(find.text('Screen B'), findsOneWidget);
+
+      routeState!.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(statefulWidgetKey.currentState?.counter, equals(1));
+
+      router.go('/a');
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsOneWidget);
+      expect(find.text('Screen A Detail'), findsNothing);
+      router.go('/a/detailA');
+      await tester.pumpAndSettle();
+      expect(statefulWidgetKey.currentState?.counter, equals(0));
+    });
+
+    testWidgets('Maintains state for nested StatefulShellRoute',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<DummyStatefulWidgetState> statefulWidgetKey =
+          GlobalKey<DummyStatefulWidgetState>();
+      StatefulNavigationShell? routeState1;
+      StatefulNavigationShell? routeState2;
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState1 = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(routes: <RouteBase>[
+              StatefulShellRoute.indexedStack(
+                  builder: (BuildContext context, GoRouterState state,
+                      StatefulNavigationShell navigationShell) {
+                    routeState2 = navigationShell;
+                    return navigationShell;
+                  },
+                  branches: <StatefulShellBranch>[
+                    StatefulShellBranch(routes: <RouteBase>[
+                      GoRoute(
+                        path: '/a',
+                        builder: (BuildContext context, GoRouterState state) =>
+                            const Text('Screen A'),
+                        routes: <RouteBase>[
+                          GoRoute(
+                            path: 'detailA',
+                            builder:
+                                (BuildContext context, GoRouterState state) =>
+                                    Column(children: <Widget>[
+                              const Text('Screen A Detail'),
+                              DummyStatefulWidget(key: statefulWidgetKey),
+                            ]),
+                          ),
+                        ],
+                      ),
+                    ]),
+                    StatefulShellBranch(routes: <RouteBase>[
+                      GoRoute(
+                        path: '/b',
+                        builder: (BuildContext context, GoRouterState state) =>
+                            const Text('Screen B'),
+                      ),
+                    ]),
+                    StatefulShellBranch(routes: <RouteBase>[
+                      GoRoute(
+                        path: '/c',
+                        builder: (BuildContext context, GoRouterState state) =>
+                            const Text('Screen C'),
+                      ),
+                    ]),
+                  ]),
+            ]),
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/d',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen D'),
+              ),
+            ]),
+          ],
+        ),
+      ];
+
+      await createRouter(routes, tester,
+          initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey);
+      statefulWidgetKey.currentState?.increment();
+      expect(find.text('Screen A Detail'), findsOneWidget);
+      routeState2!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen B'), findsOneWidget);
+
+      routeState1!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen D'), findsOneWidget);
+
+      routeState1!.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen B'), findsOneWidget);
+
+      routeState2!.goBranch(2);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen C'), findsOneWidget);
+
+      routeState2!.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A Detail'), findsOneWidget);
+      expect(statefulWidgetKey.currentState?.counter, equals(1));
+    });
+
+    testWidgets(
+        'Pops from the correct Navigator in a StatefulShellRoute when the '
+        'Android back button is pressed', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<NavigatorState> sectionANavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<NavigatorState> sectionBNavigatorKey =
+          GlobalKey<NavigatorState>();
+      StatefulNavigationShell? routeState;
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(
+                navigatorKey: sectionANavigatorKey,
+                routes: <GoRoute>[
+                  GoRoute(
+                    path: '/a',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const Text('Screen A'),
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'detailA',
+                        builder: (BuildContext context, GoRouterState state) =>
+                            const Text('Screen A Detail'),
+                      ),
+                    ],
+                  ),
+                ]),
+            StatefulShellBranch(
+                navigatorKey: sectionBNavigatorKey,
+                routes: <GoRoute>[
+                  GoRoute(
+                    path: '/b',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const Text('Screen B'),
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'detailB',
+                        builder: (BuildContext context, GoRouterState state) =>
+                            const Text('Screen B Detail'),
+                      ),
+                    ],
+                  ),
+                ]),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/a/detailA', navigatorKey: rootNavigatorKey);
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen A Detail'), findsOneWidget);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Screen B Detail'), findsNothing);
+
+      router.go('/b/detailB');
+      await tester.pumpAndSettle();
+
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen A Detail'), findsNothing);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Screen B Detail'), findsOneWidget);
+
+      await simulateAndroidBackButton(tester);
+      await tester.pumpAndSettle();
+
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen A Detail'), findsNothing);
+      expect(find.text('Screen B'), findsOneWidget);
+      expect(find.text('Screen B Detail'), findsNothing);
+
+      routeState!.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen A Detail'), findsOneWidget);
+
+      await simulateAndroidBackButton(tester);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsOneWidget);
+      expect(find.text('Screen A Detail'), findsNothing);
+    });
+
+    testWidgets(
+        'Maintains extra navigation information when navigating '
+        'between branches in StatefulShellRoute', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      StatefulNavigationShell? routeState;
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen A'),
+              ),
+            ]),
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/b',
+                builder: (BuildContext context, GoRouterState state) =>
+                    Text('Screen B - ${state.extra}'),
+              ),
+            ]),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/a', navigatorKey: rootNavigatorKey);
+      expect(find.text('Screen A'), findsOneWidget);
+
+      router.go('/b', extra: 'X');
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B - X'), findsOneWidget);
+
+      routeState!.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsOneWidget);
+      expect(find.text('Screen B - X'), findsNothing);
+
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B - X'), findsOneWidget);
+    });
+
+    testWidgets(
+        'Pushed non-descendant routes are correctly restored when '
+        'navigating between branches in StatefulShellRoute',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      StatefulNavigationShell? routeState;
+
+      final List<RouteBase> routes = <RouteBase>[
+        GoRoute(
+          path: '/common',
+          builder: (BuildContext context, GoRouterState state) =>
+              Text('Common - ${state.extra}'),
+        ),
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen A'),
+              ),
+            ]),
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/b',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen B'),
+              ),
+            ]),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/a', navigatorKey: rootNavigatorKey);
+      expect(find.text('Screen A'), findsOneWidget);
+
+      router.go('/b');
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B'), findsOneWidget);
+
+      router.push('/common', extra: 'X');
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Common - X'), findsOneWidget);
+
+      routeState!.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsOneWidget);
+
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Common - X'), findsOneWidget);
+    });
+
+    testWidgets(
+        'Redirects are correctly handled when switching branch in a '
+        'StatefulShellRoute', (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      StatefulNavigationShell? routeState;
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen A'),
+              ),
+            ]),
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/b',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen B'),
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: 'details1',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const Text('Screen B Detail1'),
+                  ),
+                  GoRoute(
+                    path: 'details2',
+                    builder: (BuildContext context, GoRouterState state) =>
+                        const Text('Screen B Detail2'),
+                  ),
+                ],
+              ),
+            ]),
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/c',
+                redirect: (_, __) => '/c/main2',
+              ),
+              GoRoute(
+                path: '/c/main1',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen C1'),
+              ),
+              GoRoute(
+                path: '/c/main2',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen C2'),
+              ),
+            ]),
+          ],
+        ),
+      ];
+
+      String redirectDestinationBranchB = '/b/details1';
+      await createRouter(
+        routes,
+        tester,
+        initialLocation: '/a',
+        navigatorKey: rootNavigatorKey,
+        redirect: (_, GoRouterState state) {
+          if (state.location.startsWith('/b')) {
+            return redirectDestinationBranchB;
+          }
+          return null;
+        },
+      );
+      expect(find.text('Screen A'), findsOneWidget);
+      expect(find.text('Screen B Detail'), findsNothing);
+
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B Detail1'), findsOneWidget);
+
+      routeState!.goBranch(2);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B Detail1'), findsNothing);
+      expect(find.text('Screen C2'), findsOneWidget);
+
+      redirectDestinationBranchB = '/b/details2';
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B Detail2'), findsOneWidget);
+      expect(find.text('Screen C2'), findsNothing);
+    });
+
+    testWidgets(
+        'Pushed top-level route is correctly handled by StatefulShellRoute',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<NavigatorState> nestedNavigatorKey =
+          GlobalKey<NavigatorState>();
+      StatefulNavigationShell? routeState;
+
+      final List<RouteBase> routes = <RouteBase>[
+        // First level shell
+        StatefulShellRoute.indexedStack(
+          builder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return navigationShell;
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/a',
+                builder: (BuildContext context, GoRouterState state) =>
+                    const Text('Screen A'),
+              ),
+            ]),
+            StatefulShellBranch(routes: <RouteBase>[
+              // Second level / nested shell
+              StatefulShellRoute.indexedStack(
+                builder: (BuildContext context, GoRouterState state,
+                        StatefulNavigationShell navigationShell) =>
+                    navigationShell,
+                branches: <StatefulShellBranch>[
+                  StatefulShellBranch(routes: <GoRoute>[
+                    GoRoute(
+                      path: '/b1',
+                      builder: (BuildContext context, GoRouterState state) =>
+                          const Text('Screen B1'),
+                    ),
+                  ]),
+                  StatefulShellBranch(
+                      navigatorKey: nestedNavigatorKey,
+                      routes: <GoRoute>[
+                        GoRoute(
+                          path: '/b2',
+                          builder:
+                              (BuildContext context, GoRouterState state) =>
+                                  const Text('Screen B2'),
+                        ),
+                        GoRoute(
+                          path: '/b2-modal',
+                          // We pass an explicit parentNavigatorKey here, to
+                          // properly test the logic in RouteBuilder, i.e.
+                          // routes with parentNavigatorKeys under the shell
+                          // should not be stripped.
+                          parentNavigatorKey: nestedNavigatorKey,
+                          builder:
+                              (BuildContext context, GoRouterState state) =>
+                                  const Text('Nested Modal'),
+                        ),
+                      ]),
+                ],
+              ),
+            ]),
+          ],
+        ),
+        GoRoute(
+          path: '/top-modal',
+          parentNavigatorKey: rootNavigatorKey,
+          builder: (BuildContext context, GoRouterState state) =>
+              const Text('Top Modal'),
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/a', navigatorKey: rootNavigatorKey);
+      expect(find.text('Screen A'), findsOneWidget);
+
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen B1'), findsOneWidget);
+
+      // Navigate nested (second level) shell to second branch
+      router.go('/b2');
+      await tester.pumpAndSettle();
+      expect(find.text('Screen B2'), findsOneWidget);
+
+      // Push route over second branch of nested (second level) shell
+      router.push('/b2-modal');
+      await tester.pumpAndSettle();
+      expect(find.text('Nested Modal'), findsOneWidget);
+
+      // Push top-level route while on second branch
+      router.push('/top-modal');
+      await tester.pumpAndSettle();
+      expect(find.text('Top Modal'), findsOneWidget);
+
+      // Return to shell and first branch
+      router.go('/a');
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsOneWidget);
+
+      // Switch to second branch, which should only contain 'Nested Modal'
+      // (in the nested shell)
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A'), findsNothing);
+      expect(find.text('Screen B1'), findsNothing);
+      expect(find.text('Screen B2'), findsNothing);
+      expect(find.text('Top Modal'), findsNothing);
+      expect(find.text('Nested Modal'), findsOneWidget);
+    });
   });
 
   group('Imperative navigation', () {
@@ -3113,44 +3954,47 @@
       );
 
       testWidgets(
-        'It checks if ShellRoute navigators can pop',
+        'It checks if StatefulShellRoute navigators can pop',
         (WidgetTester tester) async {
-          final GlobalKey<NavigatorState> shellNavigatorKey =
+          final GlobalKey<NavigatorState> rootNavigatorKey =
               GlobalKey<NavigatorState>();
           final GoRouter router = GoRouter(
+            navigatorKey: rootNavigatorKey,
             initialLocation: '/a',
             routes: <RouteBase>[
-              ShellRoute(
-                navigatorKey: shellNavigatorKey,
-                builder:
-                    (BuildContext context, GoRouterState state, Widget child) {
-                  return Scaffold(
-                    appBar: AppBar(title: const Text('Shell')),
-                    body: child,
-                  );
-                },
-                routes: <GoRoute>[
-                  GoRoute(
-                    path: '/a',
-                    builder: (BuildContext context, _) {
-                      return Scaffold(
-                        body: TextButton(
-                          onPressed: () async {
-                            shellNavigatorKey.currentState!.push(
-                              MaterialPageRoute<void>(
-                                builder: (BuildContext context) {
-                                  return const Scaffold(
-                                    body: Text('pageless route'),
-                                  );
-                                },
-                              ),
+              StatefulShellRoute.indexedStack(
+                builder: mockStackedShellBuilder,
+                branches: <StatefulShellBranch>[
+                  StatefulShellBranch(routes: <GoRoute>[
+                    GoRoute(
+                      path: '/a',
+                      builder: (BuildContext context, _) {
+                        return const Scaffold(
+                          body: Text('Screen A'),
+                        );
+                      },
+                    ),
+                  ]),
+                  StatefulShellBranch(routes: <GoRoute>[
+                    GoRoute(
+                      path: '/b',
+                      builder: (BuildContext context, _) {
+                        return const Scaffold(
+                          body: Text('Screen B'),
+                        );
+                      },
+                      routes: <RouteBase>[
+                        GoRoute(
+                          path: 'detail',
+                          builder: (BuildContext context, _) {
+                            return const Scaffold(
+                              body: Text('Screen B detail'),
                             );
                           },
-                          child: const Text('Push'),
                         ),
-                      );
-                    },
-                  ),
+                      ],
+                    ),
+                  ]),
                 ],
               ),
             ],
@@ -3164,16 +4008,19 @@
           );
 
           expect(router.canPop(), false);
-          expect(find.text('Push'), findsOneWidget);
 
-          await tester.tap(find.text('Push'));
+          router.go('/b/detail');
           await tester.pumpAndSettle();
 
-          expect(
-              find.text('pageless route', skipOffstage: false), findsOneWidget);
+          expect(find.text('Screen B detail', skipOffstage: false),
+              findsOneWidget);
           expect(router.canPop(), true);
+          // Verify that it is actually the StatefulShellRoute that reports
+          // canPop = true
+          expect(rootNavigatorKey.currentState?.canPop(), false);
         },
       );
+
       testWidgets('Pageless route should include in can pop',
           (WidgetTester tester) async {
         final GlobalKey<NavigatorState> root =
@@ -3483,6 +4330,321 @@
       },
     );
   });
+
+  group('state restoration', () {
+    testWidgets('Restores state correctly', (WidgetTester tester) async {
+      final GlobalKey<DummyRestorableStatefulWidgetState> statefulWidgetKeyA =
+          GlobalKey<DummyRestorableStatefulWidgetState>();
+
+      final List<RouteBase> routes = <RouteBase>[
+        GoRoute(
+          path: '/a',
+          pageBuilder: createPageBuilder(
+              restorationId: 'screenA', child: const Text('Screen A')),
+          routes: <RouteBase>[
+            GoRoute(
+              path: 'detail',
+              pageBuilder: createPageBuilder(
+                  restorationId: 'screenADetail',
+                  child: Column(children: <Widget>[
+                    const Text('Screen A Detail'),
+                    DummyRestorableStatefulWidget(
+                        key: statefulWidgetKeyA, restorationId: 'counterA'),
+                  ])),
+            ),
+          ],
+        ),
+      ];
+
+      await createRouter(routes, tester,
+          initialLocation: '/a/detail', restorationScopeId: 'test');
+      await tester.pumpAndSettle();
+      statefulWidgetKeyA.currentState?.increment();
+      expect(statefulWidgetKeyA.currentState?.counter, equals(1));
+      await tester.pumpAndSettle(); // Give state change time to persist
+
+      await tester.restartAndRestore();
+
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A Detail'), findsOneWidget);
+      expect(statefulWidgetKeyA.currentState?.counter, equals(1));
+    });
+
+    testWidgets('Restores state of branches in StatefulShellRoute correctly',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<DummyRestorableStatefulWidgetState> statefulWidgetKeyA =
+          GlobalKey<DummyRestorableStatefulWidgetState>();
+      final GlobalKey<DummyRestorableStatefulWidgetState> statefulWidgetKeyB =
+          GlobalKey<DummyRestorableStatefulWidgetState>();
+      final GlobalKey<DummyRestorableStatefulWidgetState> statefulWidgetKeyC =
+          GlobalKey<DummyRestorableStatefulWidgetState>();
+      StatefulNavigationShell? routeState;
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          restorationScopeId: 'shell',
+          pageBuilder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeState = navigationShell;
+            return MaterialPage<dynamic>(
+                restorationId: 'shellWidget', child: navigationShell);
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(
+                restorationScopeId: 'branchA',
+                routes: <GoRoute>[
+                  GoRoute(
+                    path: '/a',
+                    pageBuilder: createPageBuilder(
+                        restorationId: 'screenA',
+                        child: const Text('Screen A')),
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'detailA',
+                        pageBuilder: createPageBuilder(
+                            restorationId: 'screenADetail',
+                            child: Column(children: <Widget>[
+                              const Text('Screen A Detail'),
+                              DummyRestorableStatefulWidget(
+                                  key: statefulWidgetKeyA,
+                                  restorationId: 'counterA'),
+                            ])),
+                      ),
+                    ],
+                  ),
+                ]),
+            StatefulShellBranch(
+                restorationScopeId: 'branchB',
+                routes: <GoRoute>[
+                  GoRoute(
+                    path: '/b',
+                    pageBuilder: createPageBuilder(
+                        restorationId: 'screenB',
+                        child: const Text('Screen B')),
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'detailB',
+                        pageBuilder: createPageBuilder(
+                            restorationId: 'screenBDetail',
+                            child: Column(children: <Widget>[
+                              const Text('Screen B Detail'),
+                              DummyRestorableStatefulWidget(
+                                  key: statefulWidgetKeyB,
+                                  restorationId: 'counterB'),
+                            ])),
+                      ),
+                    ],
+                  ),
+                ]),
+            StatefulShellBranch(routes: <GoRoute>[
+              GoRoute(
+                path: '/c',
+                pageBuilder: createPageBuilder(
+                    restorationId: 'screenC', child: const Text('Screen C')),
+                routes: <RouteBase>[
+                  GoRoute(
+                    path: 'detailC',
+                    pageBuilder: createPageBuilder(
+                        restorationId: 'screenCDetail',
+                        child: Column(children: <Widget>[
+                          const Text('Screen C Detail'),
+                          DummyRestorableStatefulWidget(
+                              key: statefulWidgetKeyC,
+                              restorationId: 'counterC'),
+                        ])),
+                  ),
+                ],
+              ),
+            ]),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/a/detailA',
+          navigatorKey: rootNavigatorKey,
+          restorationScopeId: 'test');
+      await tester.pumpAndSettle();
+      statefulWidgetKeyA.currentState?.increment();
+      expect(statefulWidgetKeyA.currentState?.counter, equals(1));
+
+      router.go('/b/detailB');
+      await tester.pumpAndSettle();
+      statefulWidgetKeyB.currentState?.increment();
+      expect(statefulWidgetKeyB.currentState?.counter, equals(1));
+
+      router.go('/c/detailC');
+      await tester.pumpAndSettle();
+      statefulWidgetKeyC.currentState?.increment();
+      expect(statefulWidgetKeyC.currentState?.counter, equals(1));
+
+      routeState!.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A Detail'), findsOneWidget);
+
+      await tester.restartAndRestore();
+
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A Detail'), findsOneWidget);
+      expect(statefulWidgetKeyA.currentState?.counter, equals(1));
+
+      routeState!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen B Detail'), findsOneWidget);
+      expect(statefulWidgetKeyB.currentState?.counter, equals(1));
+
+      routeState!.goBranch(2);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen C Detail'), findsOneWidget);
+      // State of branch C should not have been restored
+      expect(statefulWidgetKeyC.currentState?.counter, equals(0));
+    });
+
+    testWidgets(
+        'Restores state of imperative routes in StatefulShellRoute correctly',
+        (WidgetTester tester) async {
+      final GlobalKey<NavigatorState> rootNavigatorKey =
+          GlobalKey<NavigatorState>();
+      final GlobalKey<DummyRestorableStatefulWidgetState> statefulWidgetKeyA =
+          GlobalKey<DummyRestorableStatefulWidgetState>();
+      final GlobalKey<DummyRestorableStatefulWidgetState> statefulWidgetKeyB =
+          GlobalKey<DummyRestorableStatefulWidgetState>();
+      StatefulNavigationShell? routeStateRoot;
+      StatefulNavigationShell? routeStateNested;
+
+      final List<RouteBase> routes = <RouteBase>[
+        StatefulShellRoute.indexedStack(
+          restorationScopeId: 'shell',
+          pageBuilder: (BuildContext context, GoRouterState state,
+              StatefulNavigationShell navigationShell) {
+            routeStateRoot = navigationShell;
+            return MaterialPage<dynamic>(
+                restorationId: 'shellWidget', child: navigationShell);
+          },
+          branches: <StatefulShellBranch>[
+            StatefulShellBranch(
+                restorationScopeId: 'branchA',
+                routes: <GoRoute>[
+                  GoRoute(
+                    path: '/a',
+                    pageBuilder: createPageBuilder(
+                        restorationId: 'screenA',
+                        child: const Text('Screen A')),
+                    routes: <RouteBase>[
+                      GoRoute(
+                        path: 'detailA',
+                        pageBuilder: createPageBuilder(
+                            restorationId: 'screenADetail',
+                            child: Column(children: <Widget>[
+                              const Text('Screen A Detail'),
+                              DummyRestorableStatefulWidget(
+                                  key: statefulWidgetKeyA,
+                                  restorationId: 'counterA'),
+                            ])),
+                      ),
+                    ],
+                  ),
+                ]),
+            StatefulShellBranch(
+                restorationScopeId: 'branchB',
+                routes: <RouteBase>[
+                  StatefulShellRoute.indexedStack(
+                      restorationScopeId: 'branchB-nested-shell',
+                      pageBuilder: (BuildContext context, GoRouterState state,
+                          StatefulNavigationShell navigationShell) {
+                        routeStateNested = navigationShell;
+                        return MaterialPage<dynamic>(
+                            restorationId: 'shellWidget-nested',
+                            child: navigationShell);
+                      },
+                      branches: <StatefulShellBranch>[
+                        StatefulShellBranch(
+                            restorationScopeId: 'branchB-nested',
+                            routes: <GoRoute>[
+                              GoRoute(
+                                path: '/b',
+                                pageBuilder: createPageBuilder(
+                                    restorationId: 'screenB',
+                                    child: const Text('Screen B')),
+                                routes: <RouteBase>[
+                                  GoRoute(
+                                    path: 'detailB',
+                                    pageBuilder: createPageBuilder(
+                                        restorationId: 'screenBDetail',
+                                        child: Column(children: <Widget>[
+                                          const Text('Screen B Detail'),
+                                          DummyRestorableStatefulWidget(
+                                              key: statefulWidgetKeyB,
+                                              restorationId: 'counterB'),
+                                        ])),
+                                  ),
+                                ],
+                              ),
+                            ]),
+                        StatefulShellBranch(
+                            restorationScopeId: 'branchC-nested',
+                            routes: <GoRoute>[
+                              GoRoute(
+                                path: '/c',
+                                pageBuilder: createPageBuilder(
+                                    restorationId: 'screenC',
+                                    child: const Text('Screen C')),
+                              ),
+                            ]),
+                      ])
+                ]),
+          ],
+        ),
+      ];
+
+      final GoRouter router = await createRouter(routes, tester,
+          initialLocation: '/a/detailA',
+          navigatorKey: rootNavigatorKey,
+          restorationScopeId: 'test');
+      await tester.pumpAndSettle();
+      statefulWidgetKeyA.currentState?.increment();
+      expect(statefulWidgetKeyA.currentState?.counter, equals(1));
+
+      routeStateRoot!.goBranch(1);
+      await tester.pumpAndSettle();
+
+      router.go('/b/detailB');
+      await tester.pumpAndSettle();
+      statefulWidgetKeyB.currentState?.increment();
+      expect(statefulWidgetKeyB.currentState?.counter, equals(1));
+
+      routeStateRoot!.goBranch(0);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A Detail'), findsOneWidget);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Screen B Pushed Detail'), findsNothing);
+
+      await tester.restartAndRestore();
+
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A Detail'), findsOneWidget);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Screen B Pushed Detail'), findsNothing);
+      expect(statefulWidgetKeyA.currentState?.counter, equals(1));
+
+      routeStateRoot!.goBranch(1);
+      await tester.pumpAndSettle();
+      expect(find.text('Screen A Detail'), findsNothing);
+      expect(find.text('Screen B'), findsNothing);
+      expect(find.text('Screen B Detail'), findsOneWidget);
+      expect(statefulWidgetKeyB.currentState?.counter, equals(1));
+
+      routeStateNested!.goBranch(1);
+      await tester.pumpAndSettle();
+      routeStateNested!.goBranch(0);
+      await tester.pumpAndSettle();
+
+      expect(find.text('Screen B Detail'), findsOneWidget);
+      expect(statefulWidgetKeyB.currentState?.counter, equals(1));
+    });
+  });
 }
 
 class TestInheritedNotifier extends InheritedNotifier<ValueNotifier<String>> {
diff --git a/packages/go_router/test/matching_test.dart b/packages/go_router/test/matching_test.dart
index daf8ccc..8a1d12a 100644
--- a/packages/go_router/test/matching_test.dart
+++ b/packages/go_router/test/matching_test.dart
@@ -74,4 +74,37 @@
     expect(matches1 == matches2, isTrue);
     expect(matches1 == matches3, isFalse);
   });
+
+  test('RouteMatchList is encoded and decoded correctly', () {
+    final RouteConfiguration configuration = RouteConfiguration(
+      routes: <GoRoute>[
+        GoRoute(
+          path: '/a',
+          builder: (BuildContext context, GoRouterState state) =>
+              const Placeholder(),
+        ),
+        GoRoute(
+          path: '/b',
+          builder: (BuildContext context, GoRouterState state) =>
+              const Placeholder(),
+        ),
+      ],
+      redirectLimit: 0,
+      navigatorKey: GlobalKey<NavigatorState>(),
+      topRedirect: (_, __) => null,
+    );
+    final RouteMatcher matcher = RouteMatcher(configuration);
+    final RouteMatchListCodec codec = RouteMatchListCodec(matcher);
+
+    final RouteMatchList list1 = matcher.findMatch('/a');
+    final RouteMatchList list2 = matcher.findMatch('/b');
+    list1.push(ImperativeRouteMatch<Object?>(
+        pageKey: const ValueKey<String>('/b-p0'), matches: list2));
+
+    final Object? encoded = codec.encodeMatchList(list1);
+    final RouteMatchList? decoded = codec.decodeMatchList(encoded);
+
+    expect(decoded, isNotNull);
+    expect(decoded, equals(list1));
+  });
 }
diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart
index d2187d4..19f795c 100644
--- a/packages/go_router/test/test_helpers.dart
+++ b/packages/go_router/test/test_helpers.dart
@@ -8,6 +8,7 @@
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:go_router/go_router.dart';
+import 'package:go_router/src/match.dart';
 
 Future<GoRouter> createGoRouter(WidgetTester tester) async {
   final GoRouter goRouter = GoRouter(
@@ -147,6 +148,7 @@
   int redirectLimit = 5,
   GlobalKey<NavigatorState>? navigatorKey,
   GoRouterWidgetBuilder? errorBuilder,
+  String? restorationScopeId,
 }) async {
   final GoRouter goRouter = GoRouter(
     routes: routes,
@@ -158,9 +160,12 @@
         (BuildContext context, GoRouterState state) =>
             TestErrorScreen(state.error!),
     navigatorKey: navigatorKey,
+    restorationScopeId: restorationScopeId,
   );
   await tester.pumpWidget(
     MaterialApp.router(
+      restorationScopeId:
+          restorationScopeId != null ? '$restorationScopeId-root' : null,
       routerConfig: goRouter,
     ),
   );
@@ -228,10 +233,49 @@
   const DummyStatefulWidget({super.key});
 
   @override
-  State<DummyStatefulWidget> createState() => DummyStatefulWidgetState();
+  State<StatefulWidget> createState() => DummyStatefulWidgetState();
 }
 
 class DummyStatefulWidgetState extends State<DummyStatefulWidget> {
+  int counter = 0;
+
+  void increment() => setState(() {
+        counter++;
+      });
+
+  @override
+  Widget build(BuildContext context) => Container();
+}
+
+class DummyRestorableStatefulWidget extends StatefulWidget {
+  const DummyRestorableStatefulWidget({super.key, this.restorationId});
+
+  final String? restorationId;
+
+  @override
+  State<StatefulWidget> createState() => DummyRestorableStatefulWidgetState();
+}
+
+class DummyRestorableStatefulWidgetState
+    extends State<DummyRestorableStatefulWidget> with RestorationMixin {
+  final RestorableInt _counter = RestorableInt(0);
+
+  @override
+  String? get restorationId => widget.restorationId;
+
+  int get counter => _counter.value;
+
+  void increment([int count = 1]) => setState(() {
+        _counter.value += count;
+      });
+
+  @override
+  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
+    if (restorationId != null) {
+      registerForRestoration(_counter, restorationId!);
+    }
+  }
+
   @override
   Widget build(BuildContext context) => Container();
 }
@@ -242,3 +286,23 @@
   await tester.binding.defaultBinaryMessenger
       .handlePlatformMessage('flutter/navigation', message, (_) {});
 }
+
+GoRouterPageBuilder createPageBuilder(
+        {String? restorationId, required Widget child}) =>
+    (BuildContext context, GoRouterState state) =>
+        MaterialPage<dynamic>(restorationId: restorationId, child: child);
+
+StatefulShellRouteBuilder mockStackedShellBuilder = (BuildContext context,
+    GoRouterState state, StatefulNavigationShell navigationShell) {
+  return navigationShell;
+};
+
+RouteMatch createRouteMatch(RouteBase route, String location) {
+  return RouteMatch(
+    route: route,
+    matchedLocation: location,
+    extra: null,
+    error: null,
+    pageKey: ValueKey<String>(location),
+  );
+}