[go_router]: implemented helpers for ShellRoute (#2730)

* [Feature]: implemented helpers for ShellRoute

* [bugfix]: change export

* Removed named private constructor for TypedRoute

* Update CHANGELOG.md

* Update pubspec.yaml

* [Feature]: split routes per type

* [Feature]: add new exports

* Tests, refactor routes

* remove line in changelog

* Add navigatorKey for each route

* Fixed tests

* Format

* Bump version in pubspec.yaml

* Update packages/go_router/lib/src/route_data.dart

Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com>

* Update packages/go_router/lib/src/route_data.dart

Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com>

* Removed buildPageWithState tests

* Bump version in pubspec

* Address comments from code review

* Export ShellRouteData

* Remove unnecessary import

---------

Co-authored-by: chunhtai <47866232+chunhtai@users.noreply.github.com>
Co-authored-by: John Ryan <ryjohn@google.com>
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index 5b9c7d4..e7346b2 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 6.0.10
+
+- Adds helpers for go_router_builder for ShellRoute support
+
 ## 6.0.9
 
 - Fixes deprecation message for `GoRouterState.namedLocation`
diff --git a/packages/go_router/lib/go_router.dart b/packages/go_router/lib/go_router.dart
index ad2a74f..b60ae60 100644
--- a/packages/go_router/lib/go_router.dart
+++ b/packages/go_router/lib/go_router.dart
@@ -11,7 +11,8 @@
 export 'src/misc/extensions.dart';
 export 'src/misc/inherited_router.dart';
 export 'src/pages/custom_transition_page.dart';
-export 'src/route_data.dart' show GoRouteData, TypedGoRoute;
+export 'src/route_data.dart'
+    show GoRouteData, TypedGoRoute, TypedShellRoute, ShellRouteData;
 export 'src/router.dart';
 export 'src/typedefs.dart'
     show GoRouterPageBuilder, GoRouterRedirect, GoRouterWidgetBuilder;
diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart
index d577340..3530ca2 100644
--- a/packages/go_router/lib/src/route_data.dart
+++ b/packages/go_router/lib/src/route_data.dart
@@ -11,13 +11,19 @@
 import 'route.dart';
 import 'state.dart';
 
+/// A superclass for each route data
+abstract class RouteData {
+  /// Default const constructor
+  const RouteData();
+}
+
 /// Baseclass for supporting
 /// [Type-safe routing](https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html).
 ///
 /// Subclasses must override one of [build], [buildPage], or
 /// [redirect].
 /// {@category Type-safe routes}
-abstract class GoRouteData {
+abstract class GoRouteData extends RouteData {
   /// Allows subclasses to have `const` constructors.
   ///
   /// [GoRouteData] is abstract and cannot be instantiated directly.
@@ -74,7 +80,8 @@
   static GoRoute $route<T extends GoRouteData>({
     required String path,
     required T Function(GoRouterState) factory,
-    List<GoRoute> routes = const <GoRoute>[],
+    GlobalKey<NavigatorState>? parentNavigatorKey,
+    List<RouteBase> routes = const <RouteBase>[],
   }) {
     T factoryImpl(GoRouterState state) {
       final Object? extra = state.extra;
@@ -103,6 +110,7 @@
       pageBuilder: pageBuilder,
       redirect: redirect,
       routes: routes,
+      parentNavigatorKey: parentNavigatorKey,
     );
   }
 
@@ -111,26 +119,138 @@
   static final Expando<GoRouteData> _stateObjectExpando = Expando<GoRouteData>(
     'GoRouteState to GoRouteData expando',
   );
+
+  /// [navigatorKey] is used to point to a certain navigator
+  ///
+  /// It will use the given key to find the right navigator for [GoRoute]
+  GlobalKey<NavigatorState>? get navigatorKey => null;
 }
 
-/// Annotation for types that support typed routing.
+/// Base class for supporting
+/// [nested navigation](https://pub.dev/packages/go_router#nested-navigation)
+abstract class ShellRouteData extends RouteData {
+  /// Default const constructor
+  const ShellRouteData();
+
+  /// [pageBuilder] is used to build the page
+  Page<void> pageBuilder(
+    BuildContext context,
+    GoRouterState state,
+    Widget navigator,
+  ) =>
+      const NoOpPage();
+
+  /// [pageBuilder] is used to build the page
+  Widget builder(
+    BuildContext context,
+    GoRouterState state,
+    Widget navigator,
+  ) =>
+      throw UnimplementedError(
+        'One of `builder` or `pageBuilder` must be implemented.',
+      );
+
+  /// A helper function used by generated code.
+  ///
+  /// Should not be used directly.
+  static ShellRoute $route<T extends ShellRouteData>({
+    required T Function(GoRouterState) factory,
+    GlobalKey<NavigatorState>? navigatorKey,
+    List<RouteBase> routes = const <RouteBase>[],
+  }) {
+    T factoryImpl(GoRouterState state) {
+      final Object? extra = state.extra;
+
+      // If the "extra" value is of type `T` then we know it's the source
+      // instance of `GoRouteData`, so it doesn't need to be recreated.
+      if (extra is T) {
+        return extra;
+      }
+
+      return (_stateObjectExpando[state] ??= factory(state)) as T;
+    }
+
+    Widget builder(
+      BuildContext context,
+      GoRouterState state,
+      Widget navigator,
+    ) =>
+        factoryImpl(state).builder(
+          context,
+          state,
+          navigator,
+        );
+
+    Page<void> pageBuilder(
+      BuildContext context,
+      GoRouterState state,
+      Widget navigator,
+    ) =>
+        factoryImpl(state).pageBuilder(
+          context,
+          state,
+          navigator,
+        );
+
+    return ShellRoute(
+      builder: builder,
+      pageBuilder: pageBuilder,
+      routes: routes,
+      navigatorKey: navigatorKey,
+    );
+  }
+
+  /// Used to cache [ShellRouteData] that corresponds to a given [GoRouterState]
+  /// to minimize the number of times it has to be deserialized.
+  static final Expando<ShellRouteData> _stateObjectExpando =
+      Expando<ShellRouteData>(
+    'GoRouteState to ShellRouteData expando',
+  );
+
+  /// It will be used to instantiate [Navigator] with the given key
+  GlobalKey<NavigatorState>? get navigatorKey => null;
+}
+
+/// A superclass for each typed route descendant
+class TypedRoute<T extends RouteData> {
+  /// Default const constructor
+  const TypedRoute();
+}
+
+/// A superclass for each typed go route descendant
 @Target(<TargetKind>{TargetKind.library, TargetKind.classType})
-class TypedGoRoute<T extends GoRouteData> {
-  /// Instantiates a new instance of [TypedGoRoute].
+class TypedGoRoute<T extends GoRouteData> extends TypedRoute<T> {
+  /// Default const constructor
   const TypedGoRoute({
     required this.path,
-    this.routes = const <TypedGoRoute<GoRouteData>>[],
+    this.routes = const <TypedRoute<RouteData>>[],
   });
 
-  /// The path that corresponds to this rout.
+  /// The path that corresponds to this route.
   ///
   /// See [GoRoute.path].
+  ///
+  ///
   final String path;
 
   /// Child route definitions.
   ///
-  /// See [GoRoute.routes].
-  final List<TypedGoRoute<GoRouteData>> routes;
+  /// See [RouteBase.routes].
+  final List<TypedRoute<RouteData>> routes;
+}
+
+/// A superclass for each typed shell route descendant
+@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
+class TypedShellRoute<T extends ShellRouteData> extends TypedRoute<T> {
+  /// Default const constructor
+  const TypedShellRoute({
+    this.routes = const <TypedRoute<RouteData>>[],
+  });
+
+  /// Child route definitions.
+  ///
+  /// See [RouteBase.routes].
+  final List<TypedRoute<RouteData>> routes;
 }
 
 /// Internal class used to signal that the default page behavior should be used.
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index eddc355..0fbec80 100644
--- a/packages/go_router/pubspec.yaml
+++ b/packages/go_router/pubspec.yaml
@@ -1,7 +1,7 @@
 name: go_router
 description: A declarative router for Flutter based on Navigation 2 supporting
   deep linking, data-driven routes and more
-version: 6.0.9
+version: 6.0.10
 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/route_data_test.dart b/packages/go_router/test/route_data_test.dart
index 6c8a49a..1541940 100644
--- a/packages/go_router/test/route_data_test.dart
+++ b/packages/go_router/test/route_data_test.dart
@@ -15,11 +15,36 @@
       const SizedBox(key: Key('build'));
 }
 
+class _ShellRouteDataBuilder extends ShellRouteData {
+  const _ShellRouteDataBuilder();
+
+  @override
+  Widget builder(
+    BuildContext context,
+    GoRouterState state,
+    Widget navigator,
+  ) =>
+      SizedBox(
+        key: const Key('builder'),
+        child: navigator,
+      );
+}
+
 final GoRoute _goRouteDataBuild = GoRouteData.$route(
   path: '/build',
   factory: (GoRouterState state) => const _GoRouteDataBuild(),
 );
 
+final ShellRoute _shellRouteDataBuilder = ShellRouteData.$route(
+  factory: (GoRouterState state) => const _ShellRouteDataBuilder(),
+  routes: <RouteBase>[
+    GoRouteData.$route(
+      path: '/child',
+      factory: (GoRouterState state) => const _GoRouteDataBuild(),
+    ),
+  ],
+);
+
 class _GoRouteDataBuildPage extends GoRouteData {
   const _GoRouteDataBuildPage();
   @override
@@ -29,11 +54,38 @@
       );
 }
 
+class _ShellRouteDataPageBuilder extends ShellRouteData {
+  const _ShellRouteDataPageBuilder();
+
+  @override
+  Page<void> pageBuilder(
+    BuildContext context,
+    GoRouterState state,
+    Widget navigator,
+  ) =>
+      MaterialPage<void>(
+        child: SizedBox(
+          key: const Key('page-builder'),
+          child: navigator,
+        ),
+      );
+}
+
 final GoRoute _goRouteDataBuildPage = GoRouteData.$route(
   path: '/build-page',
   factory: (GoRouterState state) => const _GoRouteDataBuildPage(),
 );
 
+final ShellRoute _shellRouteDataPageBuilder = ShellRouteData.$route(
+  factory: (GoRouterState state) => const _ShellRouteDataPageBuilder(),
+  routes: <RouteBase>[
+    GoRouteData.$route(
+      path: '/child',
+      factory: (GoRouterState state) => const _GoRouteDataBuild(),
+    ),
+  ],
+);
+
 class _GoRouteDataRedirectPage extends GoRouteData {
   const _GoRouteDataRedirectPage();
   @override
@@ -53,56 +105,82 @@
 ];
 
 void main() {
-  testWidgets(
-    'It should build the page from the overridden build method',
-    (WidgetTester tester) async {
-      final GoRouter goRouter = GoRouter(
-        initialLocation: '/build',
-        routes: _routes,
-      );
-      await tester.pumpWidget(MaterialApp.router(
-        routeInformationProvider: goRouter.routeInformationProvider,
-        routeInformationParser: goRouter.routeInformationParser,
-        routerDelegate: goRouter.routerDelegate,
-      ));
-      expect(find.byKey(const Key('build')), findsOneWidget);
-      expect(find.byKey(const Key('buildPage')), findsNothing);
-    },
-  );
+  group('GoRouteData', () {
+    testWidgets(
+      'It should build the page from the overridden build method',
+      (WidgetTester tester) async {
+        final GoRouter goRouter = GoRouter(
+          initialLocation: '/build',
+          routes: _routes,
+        );
+        await tester.pumpWidget(MaterialApp.router(
+          routeInformationProvider: goRouter.routeInformationProvider,
+          routeInformationParser: goRouter.routeInformationParser,
+          routerDelegate: goRouter.routerDelegate,
+        ));
+        expect(find.byKey(const Key('build')), findsOneWidget);
+        expect(find.byKey(const Key('buildPage')), findsNothing);
+      },
+    );
 
-  testWidgets(
-    'It should build the page from the overridden buildPage method',
-    (WidgetTester tester) async {
-      final GoRouter goRouter = GoRouter(
-        initialLocation: '/build-page',
-        routes: _routes,
-      );
-      await tester.pumpWidget(MaterialApp.router(
-        routeInformationProvider: goRouter.routeInformationProvider,
-        routeInformationParser: goRouter.routeInformationParser,
-        routerDelegate: goRouter.routerDelegate,
-      ));
-      expect(find.byKey(const Key('build')), findsNothing);
-      expect(find.byKey(const Key('buildPage')), findsOneWidget);
-    },
-  );
+    testWidgets(
+      'It should build the page from the overridden buildPage method',
+      (WidgetTester tester) async {
+        final GoRouter goRouter = GoRouter(
+          initialLocation: '/build-page',
+          routes: _routes,
+        );
+        await tester.pumpWidget(MaterialApp.router(
+          routeInformationProvider: goRouter.routeInformationProvider,
+          routeInformationParser: goRouter.routeInformationParser,
+          routerDelegate: goRouter.routerDelegate,
+        ));
+        expect(find.byKey(const Key('build')), findsNothing);
+        expect(find.byKey(const Key('buildPage')), findsOneWidget);
+      },
+    );
+  });
 
-  testWidgets(
-    'It should build the page from the overridden buildPage method',
-    (WidgetTester tester) async {
-      final GoRouter goRouter = GoRouter(
-        initialLocation: '/build-page-with-state',
-        routes: _routes,
-      );
-      await tester.pumpWidget(MaterialApp.router(
-        routeInformationProvider: goRouter.routeInformationProvider,
-        routeInformationParser: goRouter.routeInformationParser,
-        routerDelegate: goRouter.routerDelegate,
-      ));
-      expect(find.byKey(const Key('build')), findsNothing);
-      expect(find.byKey(const Key('buildPage')), findsNothing);
-    },
-  );
+  group('ShellRouteData', () {
+    testWidgets(
+      'It should build the page from the overridden build method',
+      (WidgetTester tester) async {
+        final GoRouter goRouter = GoRouter(
+          initialLocation: '/child',
+          routes: <RouteBase>[
+            _shellRouteDataBuilder,
+          ],
+        );
+        await tester.pumpWidget(MaterialApp.router(
+          routeInformationProvider: goRouter.routeInformationProvider,
+          routeInformationParser: goRouter.routeInformationParser,
+          routerDelegate: goRouter.routerDelegate,
+        ));
+        expect(find.byKey(const Key('builder')), findsOneWidget);
+        expect(find.byKey(const Key('page-builder')), findsNothing);
+      },
+    );
+
+    testWidgets(
+      'It should build the page from the overridden buildPage method',
+      (WidgetTester tester) async {
+        final GoRouter goRouter = GoRouter(
+          initialLocation: '/child',
+          routes: <RouteBase>[
+            _shellRouteDataPageBuilder,
+          ],
+        );
+        await tester.pumpWidget(MaterialApp.router(
+          routeInformationProvider: goRouter.routeInformationProvider,
+          routeInformationParser: goRouter.routeInformationParser,
+          routerDelegate: goRouter.routerDelegate,
+        ));
+        expect(find.byKey(const Key('builder')), findsNothing);
+        expect(find.byKey(const Key('page-builder')), findsOneWidget);
+      },
+    );
+  });
+
   testWidgets(
     'It should redirect using the overridden redirect method',
     (WidgetTester tester) async {