Created
September 10, 2024 11:13
-
-
Save tolo/939b88e4c0a8d82e487b697c6922850a to your computer and use it in GitHub Desktop.
Demo of using StatefulShellRoute in go_router in two nested levels, with modal full screen routes and deep linking
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
import 'package:flutter/material.dart'; | |
import 'package:go_router/go_router.dart'; | |
final GlobalKey<NavigatorState> _rootNavigatorKey = | |
GlobalKey<NavigatorState>(debugLabel: 'root'); | |
final GlobalKey<_PagedRootScreenState> _pagedRootScreenKey = | |
GlobalKey<_PagedRootScreenState>(debugLabel: 'pagedRootScreenState'); | |
// NOTE: This doesn't currently world, but may come soon: | |
// final GlobalKey<StatefulNavigationShellState> _rootShellStateKey = | |
// GlobalKey<StatefulNavigationShellState>(debugLabel: 'rootShellStateKey'); | |
void main() { | |
runApp(NestedTabNavigationExampleApp()); | |
} | |
/// An example demonstrating how to use nested navigators in two levels to | |
/// handle modal navigation and deep links. | |
class NestedTabNavigationExampleApp extends StatelessWidget { | |
NestedTabNavigationExampleApp({super.key}); | |
late final _tabbedShellRoute = StatefulShellRoute.indexedStack( | |
// Maybe in the future, we could have the possibility to set a path for the | |
// shell route, which could be used as sort of alias/redirect to go to the | |
// currently active branch. For example: | |
// | |
// shellRoutePath: '/app/tabs' | |
// | |
// In addition, we could possibly | |
// initialBranchIndex: 0, | |
builder: (context, state, navigationShell) { | |
return ScaffoldWithNavBar(navigationShell: navigationShell); | |
}, | |
branches: [ | |
StatefulShellBranch( | |
routes: [ | |
GoRoute( | |
path: '/app/tab1', | |
name: 'tab1', | |
builder: (context, state) => const ExamplePage( | |
title: 'tab1', subPagePath: '/app/tab1/detail'), | |
routes: [ | |
GoRoute( | |
path: 'detail', | |
name: 'tab1detail', | |
builder: (context, state) => | |
const ExamplePage(title: 'tab1 - detail'), | |
), | |
]), | |
], | |
), | |
StatefulShellBranch( | |
routes: [ | |
GoRoute( | |
path: '/app/tab2', | |
name: 'tab2', | |
builder: (context, state) => const ExamplePage( | |
title: 'tab2', subPagePath: '/app/tab2/detail'), | |
routes: [ | |
GoRoute( | |
path: 'detail', | |
name: 'tab2detail', | |
builder: (context, state) => | |
const ExamplePage(title: 'tab2 - detail'), | |
), | |
]), | |
], | |
), | |
], | |
); | |
late final _rootShellRoute = StatefulShellRoute( | |
// key: _rootShellStateKey, // Doesn't currently work, but may come soon | |
// shellRoutePath: '/app/root' // Maybe in the future - see _tabbedShellRoute above | |
builder: (context, state, navigationShell) { | |
return navigationShell; | |
}, | |
navigatorContainerBuilder: (BuildContext context, | |
StatefulNavigationShell navigationShell, List<Widget> children) { | |
return PagedRootScreen( | |
key: _pagedRootScreenKey, | |
navigationShell: navigationShell, | |
children: children, | |
); | |
}, | |
branches: [ | |
StatefulShellBranch( | |
routes: [ | |
_tabbedShellRoute, | |
], | |
), | |
StatefulShellBranch( | |
routes: [ | |
GoRoute( | |
path: '/app/notifications', | |
name: 'notifications', | |
builder: (context, state) => const NotificationsPage(), | |
), | |
], | |
), | |
], | |
); | |
late final _router = GoRouter( | |
navigatorKey: _rootNavigatorKey, | |
initialLocation: '/login', | |
routes: [ | |
GoRoute( | |
path: '/login', | |
name: 'login', | |
builder: (context, state) => const LoginPage(), | |
), | |
_rootShellRoute, | |
], | |
); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp.router( | |
title: 'Flutter Demo', | |
debugShowCheckedModeBanner: false, | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
routerConfig: _router, | |
); | |
} | |
} | |
class ScaffoldWithNavBar extends StatelessWidget { | |
const ScaffoldWithNavBar({ | |
required this.navigationShell, | |
Key? key, | |
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar')); | |
final StatefulNavigationShell navigationShell; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: navigationShell, | |
bottomNavigationBar: BottomNavigationBar( | |
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) => navigationShell.goBranch(index), | |
), | |
); | |
} | |
} | |
class LoginPage extends StatelessWidget { | |
const LoginPage({ | |
super.key, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Center( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Text('Login Page'), | |
SizedBox.square(dimension: 50), | |
ElevatedButton( | |
onPressed: () { | |
GoRouter.of(context).go('/app/tab1'); | |
}, | |
child: Text('Login')), | |
], | |
), | |
), | |
); | |
} | |
} | |
class ExamplePage extends StatelessWidget { | |
const ExamplePage({ | |
required this.title, | |
this.subPagePath, | |
super.key, | |
}); | |
final String title; | |
final String? subPagePath; | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text(title), | |
actions: [ | |
IconButton( | |
icon: const Icon(Icons.notifications), | |
onPressed: () { | |
// NOTE: Using this as a fallback... | |
_pagedRootScreenKey.currentState?.goBranch(1); | |
// ...since this doesn't work (gives us wrong StatefulShellRoute) | |
// StatefulNavigationShell.of(context).goBranch(1); | |
// But this would be better (currently not possible): | |
// _rootShellStateKey.currentState?.goBranch(1); | |
// Even better still would be if we could to this (see comments | |
// around shellRoutePath above): | |
// GoRouter.of(context).go('/app/root/1'); | |
}, | |
), | |
], | |
), | |
body: Center( | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Text('This is the \'$title\' screen.'), | |
if (subPagePath != null) ...[ | |
SizedBox.square(dimension: 50), | |
ElevatedButton( | |
onPressed: () { | |
GoRouter.of(context).go(subPagePath!); | |
}, | |
child: Text('Go to detail')), | |
], | |
], | |
), | |
), | |
); | |
} | |
} | |
class NotificationsPage extends StatelessWidget { | |
const NotificationsPage({ | |
super.key, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('Notificatons'), | |
leading: BackButton(onPressed: () { | |
StatefulNavigationShell.of(context).goBranch(0); | |
// But this would be better (currently not possible): | |
// _rootShellStateKey.currentState?.goBranch(0); | |
// Even better still would be if we could to this (see comments | |
// around shellRoutePath above): | |
// GoRouter.of(context).go('/app/root/0'); | |
// But we could also do this as a fallback: | |
//_pagedRootScreenKey.currentState?.goBranch(0); | |
}), | |
), | |
body: Center( | |
child: ListView( | |
children: [ | |
ListTile( | |
title: Text('Notification 1'), | |
), | |
ListTile( | |
title: Text('Notification 2'), | |
), | |
ListTile( | |
title: Text('Notification 3'), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
/// Builds a nested shell using a PageView. | |
class PagedRootScreen extends StatefulWidget { | |
/// Constructs a TabbedRootScreen | |
const PagedRootScreen( | |
{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() => _PagedRootScreenState(); | |
} | |
class _PagedRootScreenState extends State<PagedRootScreen> { | |
late final PageController _pageController = PageController( | |
initialPage: widget.navigationShell.currentIndex, | |
); | |
@override | |
void dispose() { | |
_pageController.dispose(); | |
super.dispose(); | |
} | |
@override | |
void didUpdateWidget(covariant PagedRootScreen oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (_pageController.hasClients) { | |
_pageController.animateToPage( | |
widget.navigationShell.currentIndex, | |
duration: const Duration(milliseconds: 200), | |
curve: Curves.easeOut, | |
); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return PageView( | |
physics: const NeverScrollableScrollPhysics(), | |
onPageChanged: (int i) => widget.navigationShell.goBranch(i), | |
controller: _pageController, | |
children: widget.children, | |
); | |
} | |
void goBranch(int index) { | |
widget.navigationShell.goBranch(index); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment