Instantly share code, notes, and snippets.
Last active
November 17, 2024 10:51
-
Star
(6)
6
You must be signed in to star a gist -
Fork
(1)
1
You must be signed in to fork a gist
-
Save tolo/b26bd0ccb89a5fa2e57ec715f8963f2a to your computer and use it in GitHub Desktop.
Example showing how to use go_router to build stateful nested navigation, using a CupertinoTabBar. Requires https://github.com/flutter/packages/pull/2650 (or https://github.com/tolo/flutter_packages/tree/nested-persistent-navigation).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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/cupertino.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 tab uses its own persistent navigator, i.e. | |
// navigation state is maintained separately for each tab. This setup also | |
// enables deep linking into nested pages. | |
// | |
// This example demonstrates how to display routes within a StackedShellRoute, | |
// that are places on separate navigators. The example also demonstrates how | |
// state is maintained when switching between different tabs (and thus branches | |
// and Navigators). | |
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>[ | |
GoRoute( | |
path: '/modal', | |
parentNavigatorKey: _rootNavigatorKey, | |
builder: (BuildContext context, GoRouterState state) => | |
const ModalScreen(), | |
), | |
StatefulShellRoute( | |
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 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) => | |
DetailsScreen(label: 'A', extra: state.extra), | |
), | |
], | |
), | |
], | |
), | |
/// 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. | |
// navigatorKey: _tabBNavigatorKey, | |
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'], | |
extra: state.extra, | |
), | |
), | |
], | |
), | |
], | |
), | |
/// 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( | |
/// This bottom tab uses a nested shell, wrapping sub routes in a | |
/// top TabBar. | |
branches: <StatefulShellBranch>[ | |
StatefulShellBranch(routes: <GoRoute>[ | |
GoRoute( | |
path: '/c1', | |
builder: (BuildContext context, GoRouterState state) => | |
const TabScreen( | |
label: 'C1', detailsPath: '/c1/details'), | |
routes: <RouteBase>[ | |
GoRoute( | |
path: 'details', | |
builder: | |
(BuildContext context, GoRouterState state) => | |
DetailsScreen( | |
label: 'C1', | |
extra: state.extra, | |
withScaffold: false, | |
), | |
), | |
], | |
), | |
]), | |
StatefulShellBranch(routes: <GoRoute>[ | |
GoRoute( | |
path: '/c2', | |
builder: (BuildContext context, GoRouterState state) => | |
const TabScreen( | |
label: 'C2', detailsPath: '/c2/details'), | |
routes: <RouteBase>[ | |
GoRoute( | |
path: 'details', | |
builder: | |
(BuildContext context, GoRouterState state) => | |
DetailsScreen( | |
label: 'C2', | |
extra: state.extra, | |
withScaffold: false, | |
), | |
), | |
], | |
), | |
]), | |
], | |
builder: (BuildContext context, GoRouterState state, | |
StatefulNavigationShell navigationShell) { | |
/// This nested StackedShellRoute demonstrates the use of a | |
/// custom container (TabBarView) 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 TabbedRootScreen for more details on how the children | |
/// are used in the TabBarView. | |
TabbedRootScreen( | |
navigationShell: navigationShell, children: children), | |
), | |
], | |
), | |
], | |
builder: (BuildContext context, GoRouterState state, | |
StatefulNavigationShell navigationShell) { | |
/// Builder implementation for the top shell, using a custom branch | |
/// container, like with TabbedRootScreen above. | |
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 used in the TabBarView. | |
ScaffoldWithNavBar( | |
navigationShell: navigationShell, children: children), | |
/// If it's necessary to customize the Page for StackedShellRoute, | |
/// provide a pageBuilder function instead of the builder, for example: | |
// pageBuilder: (BuildContext context, GoRouterState state, | |
// StatefulNavigationShell navigationShell) { | |
// return NoTransitionPage<dynamic>(child: navigationShell); | |
// }, | |
), | |
], | |
); | |
@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; | |
/// Body, i.e. the container for the branch Navigators. | |
final List<Widget> children; | |
@override | |
Widget build(BuildContext context) { | |
return CupertinoTabScaffold( | |
tabBar: CupertinoTabBar( | |
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 tappedIndex) => navigationShell.goBranch(tappedIndex), | |
), | |
tabBuilder: (BuildContext context, int index) { | |
return children[index]; | |
}, | |
) ; | |
} | |
} | |
/// 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('Tab root - $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'), | |
), | |
const Padding(padding: EdgeInsets.all(8)), | |
ElevatedButton( | |
onPressed: () { | |
GoRouter.of(context).push('/modal'); | |
}, | |
child: const Text('Show modal screen on ROOT navigator'), | |
), | |
const Padding(padding: EdgeInsets.all(4)), | |
ElevatedButton( | |
onPressed: () { | |
showModalBottomSheet( | |
context: context, | |
useRootNavigator: true, | |
builder: _bottomSheet); | |
}, | |
child: const Text('Show bottom sheet on ROOT navigator'), | |
), | |
const Padding(padding: EdgeInsets.all(4)), | |
ElevatedButton( | |
onPressed: () { | |
showModalBottomSheet( | |
context: context, | |
useRootNavigator: false, | |
builder: _bottomSheet); | |
}, | |
child: const Text('Show bottom sheet on CURRENT navigator'), | |
), | |
], | |
), | |
), | |
); | |
} | |
Widget _bottomSheet(BuildContext context) { | |
return Container( | |
height: 200, | |
color: Colors.amber, | |
child: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
const Text('Modal BottomSheet'), | |
ElevatedButton( | |
child: const Text('Close BottomSheet'), | |
onPressed: () => Navigator.pop(context), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
/// 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)), | |
), | |
] | |
], | |
), | |
); | |
} | |
} | |
/// Widget for a modal screen. | |
class ModalScreen extends StatelessWidget { | |
/// Creates a ModalScreen | |
const ModalScreen({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Modal'), | |
), | |
body: Center( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Text('Modal screen', style: Theme.of(context).textTheme.titleLarge), | |
const Padding(padding: EdgeInsets.all(8)), | |
ElevatedButton( | |
onPressed: () { | |
GoRouter.of(context).go('/a'); | |
}, | |
child: const Text('Go to initial section'), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
/// 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 StackedShellRoute. | |
final StatefulNavigationShell navigationShell; | |
/// The children (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('Tab root'), | |
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, 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)), | |
if (detailsPath != null) | |
TextButton( | |
onPressed: () { | |
GoRouter.of(context).go(detailsPath!); | |
}, | |
child: const Text('View details'), | |
), | |
], | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
may i ask, if in section a, i add more space and make it to a scrollable, the tabbar is above content, how to make it expand to remain space, not the tabbar ?