// 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 _rootNavigatorKey = GlobalKey(debugLabel: 'root'); final GlobalKey _sectionANavigatorKey = GlobalKey(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: [ 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: [ // 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'), ), ], ), ], ), // 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: [ 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: [ 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: [ 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: [ 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('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(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: [ 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 createState() => DetailsScreenState(); } /// The state for DetailsScreen class DetailsScreenState extends State { 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: [ 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) ...[ const Padding(padding: EdgeInsets.all(16)), TextButton( onPressed: () { GoRouter.of(context).pop(); }, child: const Text('< Back', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), ), ] ], ), ); } }