| // 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)), |
| ), |
| ] |
| ], |
| ), |
| ); |
| } |
| } |