Skip to content

Instantly share code, notes, and snippets.

@tolo
Created September 10, 2024 11:13
Show Gist options
  • Save tolo/939b88e4c0a8d82e487b697c6922850a to your computer and use it in GitHub Desktop.
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
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