// 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 _rootNavigatorKey = GlobalKey(debugLabel: 'root'); final GlobalKey _tabANavigatorKey = GlobalKey(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: [ 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 children) { // Returning a customized container for the branch // Navigators (i.e. the `List children` argument). // // See ScaffoldWithNavBar for more details on how the children // are managed (using AnimatedBranchContainer). return ScaffoldWithNavBar( navigationShell: navigationShell, children: children); }, branches: [ // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( navigatorKey: _tabANavigatorKey, 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 RootScreenA(), 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 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: [ 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 children) { // Returning a customized container for the branch // Navigators (i.e. the `List 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(routes: [ GoRoute( path: '/b1', builder: (BuildContext context, GoRouterState state) => const TabScreen( label: 'B1', detailsPath: '/b1/details'), routes: [ GoRoute( path: 'details', builder: (BuildContext context, GoRouterState state) => const DetailsScreen( label: 'B1', withScaffold: false, ), ), ], ), ]), StatefulShellBranch(routes: [ GoRoute( path: '/b2', builder: (BuildContext context, GoRouterState state) => const TabScreen( label: 'B2', detailsPath: '/b2/details'), routes: [ 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('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 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(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 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: [ 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 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.withScaffold) ...[ 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 children; @override State createState() => _TabbedRootScreenState(); } class _TabbedRootScreenState extends State 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 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: [ 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'), ), ], ), ); } }