diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 1e2df5305275..21fa68a8f217 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 14.2.2 + +- Adds section for "Stateful nested navigation" to configuration.md. + ## 14.2.1 - Makes GoRouterState lookup more robust. diff --git a/packages/go_router/doc/configuration.md b/packages/go_router/doc/configuration.md index bb3dba5e6ec5..6c4f1ec6ee4d 100644 --- a/packages/go_router/doc/configuration.md +++ b/packages/go_router/doc/configuration.md @@ -152,6 +152,105 @@ For a complete example, see the [ShellRoute sample](https://github.com/flutter/packages/tree/main/packages/go_router/example/lib/shell_route.dart) in the example/ directory. +# Stateful nested navigation +In addition to using nested navigation with for instance a BottomNavigationBar, +many apps also require that state is maintained when navigating between +destinations. To accomplish this, use [StatefulShellRoute][] instead of +`ShellRoute`. + +StatefulShellRoute creates separate `Navigator`s for each of its nested [branches](https://pub.dev/documentation/go_router/latest/go_router/StatefulShellBranch-class.html) +(i.e. parallel navigation trees), making it possible to build an app with +stateful nested navigation. The constructor [StatefulShellRoute.indexedStack](https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute/StatefulShellRoute.indexedStack.html) +provides a default implementation for managing the branch navigators, using an +`IndexedStack`. + +When using StatefulShellRoute, routes aren't configured on the shell route +itself. Instead, they are configured for each of the branches. Example: + + +```dart +branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _sectionANavigatorKey, + routes: [ + 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: [ + // 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'), + ), + ], + ), + ], + ), +``` + +Similar to ShellRoute, a builder must be provided to build the actual shell +Widget that encapsulates the branch navigation container. The latter is +implemented by the class [StatefulNavigationShell](https://pub.dev/documentation/go_router/latest/go_router/StatefulNavigationShell-class.html), +which is passed as the last argument to the builder function. Example: + + +```dart +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); + }, +``` + +Within the custom shell widget, the StatefulNavigationShell is first and +foremost used as the child, or body, of the shell. Secondly, it is also used for +handling stateful switching between branches, as well as providing the currently +active branch index. Example: + + +```dart +@override +Widget build(BuildContext context) { + return Scaffold( + // The StatefulNavigationShell from the associated StatefulShellRoute is + // directly passed as the body of the 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(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, + // Navigate to the current location of the branch at the provided index + // when tapping an item in the BottomNavigationBar. + onTap: (int index) => navigationShell.goBranch(index), + ), + ); +} +``` + +For a complete example, see the [Stateful Nested +Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) +in the example/ directory. +For further details, see the [StatefulShellRoute API +documentation](https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute-class.html). + # Initial location The initial location is shown when the app first opens and there is no deep link @@ -181,3 +280,4 @@ final _router = GoRouter( [GoRoute]: https://pub.dev/documentation/go_router/latest/go_router/GoRoute-class.html [GoRouterState]: https://pub.dev/documentation/go_router/latest/go_router/GoRouterState-class.html [ShellRoute]: https://pub.dev/documentation/go_router/latest/go_router/ShellRoute-class.html +[StatefulShellRoute]: https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute-class.html diff --git a/packages/go_router/example/lib/stateful_shell_route.dart b/packages/go_router/example/lib/stateful_shell_route.dart index 9901619d7ce9..e6f0e7a1c0c6 100644 --- a/packages/go_router/example/lib/stateful_shell_route.dart +++ b/packages/go_router/example/lib/stateful_shell_route.dart @@ -28,6 +28,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { navigatorKey: _rootNavigatorKey, initialLocation: '/a', routes: [ + // #docregion configuration-builder StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { @@ -37,6 +38,8 @@ class NestedTabNavigationExampleApp extends StatelessWidget { // branches in a stateful way. return ScaffoldWithNavBar(navigationShell: navigationShell); }, + // #enddocregion configuration-builder + // #docregion configuration-branches branches: [ // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( @@ -61,6 +64,7 @@ class NestedTabNavigationExampleApp extends StatelessWidget { ), ], ), + // #enddocregion configuration-branches // The route branch for the second tab of the bottom navigation bar. StatefulShellBranch( @@ -145,9 +149,12 @@ class ScaffoldWithNavBar extends StatelessWidget { /// The navigation shell and container for the branch Navigators. final StatefulNavigationShell navigationShell; + // #docregion configuration-custom-shell @override Widget build(BuildContext context) { return Scaffold( + // The StatefulNavigationShell from the associated StatefulShellRoute is + // directly passed as the body of the Scaffold. body: navigationShell, bottomNavigationBar: BottomNavigationBar( // Here, the items of BottomNavigationBar are hard coded. In a real @@ -160,13 +167,18 @@ class ScaffoldWithNavBar extends StatelessWidget { 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. + onTap: (int index) => navigationShell.goBranch(index), ), ); } + // #enddocregion configuration-custom-shell - /// Navigate to the current location of the branch at the provided index when - /// tapping an item in the BottomNavigationBar. + /// NOTE: For a slightly more sophisticated branch switching, change the onTap + /// handler on the BottomNavigationBar above to the following: + /// `onTap: (int index) => _onTap(context, index),` + // ignore: unused_element 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 diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 8b23e54f31a5..21f2a039562f 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -777,6 +777,8 @@ class ShellRoute extends ShellRouteBase { /// * [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. +/// +/// {@category Configuration} class StatefulShellRoute extends ShellRouteBase { /// Constructs a [StatefulShellRoute] from a list of [StatefulShellBranch]es, /// each representing a separate nested navigation tree (branch). diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index fc4f35083456..60d5443e09e2 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: 14.2.1 +version: 14.2.2 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