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