Created
June 22, 2022 07:40
-
-
Save daanporon/2356fcf269cc5cd9156f60be72ba3b27 to your computer and use it in GitHub Desktop.
Go example with subnavigation and tabbed 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
import 'package:flutter/material.dart'; | |
import 'package:go_router_prototype/go_router_prototype.dart'; | |
buildProductRoutes(String product) => ShellRoute( | |
path: '$product/:pid', | |
builder: (BuildContext context, Widget child) => ScreenWithBottomNav( | |
product: product, | |
child: child, | |
), | |
defaultRoute: | |
'/home/$product/1234/tab-a', // TODO how to handle dynamic path parameters here? | |
routes: [ | |
NestedStackRoute( | |
path: 'tab-a', | |
builder: (context) { | |
return const Screen( | |
name: 'Tab A', | |
key: ValueKey( | |
'A', | |
), | |
); | |
}, | |
), | |
ShellRoute( | |
path: 'tab-b', | |
defaultRoute: | |
'/home/$product/1234/tab-b/1', // TODO how to handle dynamic path parameters here? | |
builder: (context, child) => ScreenWithTabs( | |
child: child, | |
), | |
routes: [ | |
StackedRoute( | |
path: '1', | |
builder: (context) { | |
return const Screen( | |
name: 'B.1', | |
key: ValueKey( | |
'B.1', | |
), | |
); | |
}, | |
), | |
StackedRoute( | |
path: '2', | |
builder: (context) { | |
return const Screen( | |
name: 'B.2', | |
key: ValueKey( | |
'B.2', | |
), | |
); | |
}, | |
), | |
]), | |
NestedStackRoute( | |
path: 'tab-c', | |
builder: (context) => Screen( | |
name: 'C.1', | |
key: const ValueKey('C.1'), | |
onPressed: (context) => RouteState.of(context).goTo('2'), | |
), | |
routes: [ | |
StackedRoute( | |
path: '2', | |
builder: (context) => const Screen( | |
name: 'C.2', | |
key: ValueKey('C.2'), | |
appBarEnabled: true, | |
), | |
), | |
], | |
), | |
], | |
); | |
final GoRouter _router = GoRouter( | |
routes: <StackedRoute>[ | |
StackedRoute( | |
path: '/', | |
builder: (context) => const StartScreen(), | |
routes: <StackedRoute>[ | |
StackedRoute( | |
path: 'home', // you always need a '/' route, cannot be '/home' | |
builder: (BuildContext context) => const HomeScreen(), | |
routes: [ | |
buildProductRoutes('product-a'), | |
buildProductRoutes('product-b'), | |
], | |
), | |
], | |
), | |
], | |
); | |
void main() { | |
runApp(const App()); | |
} | |
class App extends StatelessWidget { | |
const App({ | |
Key? key, | |
}) : super(key: key); | |
// This widget is the root of your application. | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp.router( | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
routeInformationParser: _router.parser, | |
routerDelegate: _router.delegate, | |
); | |
} | |
} | |
class StartScreen extends StatelessWidget { | |
const StartScreen({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Start page'), | |
), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
'/home', | |
'/home/product-a/1234/tab-b', | |
'/home/product-a/1234/tab-b/1', | |
'/home/product-a/1234/tab-b/2', | |
'/home/product-b/456/tab-c/1/2', | |
] | |
.map( | |
(route) => ElevatedButton( | |
onPressed: () => RouteState.of(context).goTo(route), | |
child: Text(route), | |
), | |
) | |
.toList(), | |
), | |
), | |
); | |
} | |
} | |
class HomeScreen extends StatelessWidget { | |
const HomeScreen({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Home page'), | |
), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
ElevatedButton( | |
onPressed: () => RouteState.of(context).goTo('product-a/1234'), | |
child: const Text('Product A'), | |
), | |
ElevatedButton( | |
onPressed: () => RouteState.of(context).goTo('product-b/456'), | |
child: const Text('Product B'), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class ScreenWithBottomNav extends StatelessWidget { | |
final Widget child; | |
final String product; | |
const ScreenWithBottomNav({ | |
Key? key, | |
required this.child, | |
required this.product, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
final selectedIndex = _calculateSelectedIndex(context); | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('$product ${RouteState.of(context).pathParameters['pid']}'), | |
), | |
body: AnimatedSwitcher( | |
duration: const Duration(milliseconds: 300), | |
child: child, | |
), | |
bottomNavigationBar: BottomNavigationBar( | |
items: const <BottomNavigationBarItem>[ | |
BottomNavigationBarItem( | |
icon: Icon(Icons.tab), | |
label: 'Tab A', | |
), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.tab), | |
label: 'Tab B', | |
), | |
BottomNavigationBarItem( | |
icon: Icon(Icons.tab), | |
label: 'Tab C', | |
), | |
], | |
currentIndex: selectedIndex, | |
onTap: (idx) => _onItemTapped(idx, context), | |
), | |
); | |
} | |
// TODO find a generic way | |
static int _calculateSelectedIndex(BuildContext context) { | |
final route = RouteState.of(context); | |
final activeChild = route.activeChild; | |
if (activeChild != null) { | |
if (activeChild.path == 'tab-a') return 0; | |
if (activeChild.path == 'tab-b') return 1; | |
if (activeChild.path == 'tab-c') return 2; | |
} | |
return 0; | |
} | |
// TODO find a generic way | |
void _onItemTapped(int index, BuildContext context) { | |
switch (index) { | |
case 1: | |
RouteState.of(context).goTo('tab-b'); | |
break; | |
case 2: | |
RouteState.of(context).goTo('tab-c'); | |
break; | |
default: | |
RouteState.of(context).goTo('tab-a'); | |
} | |
} | |
} | |
class Screen extends StatelessWidget { | |
final String name; | |
final bool appBarEnabled; | |
final void Function(BuildContext)? onPressed; | |
const Screen({ | |
Key? key, | |
required this.name, | |
this.appBarEnabled = false, | |
this.onPressed, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: appBarEnabled ? AppBar() : null, | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Text(RouteState.of(context).route.toString()), | |
Text(RouteState.of(context).pathParameters.toString()), | |
Text(RouteState.of(context).queryParameters.toString()), | |
Text('Screen $name'), | |
if (onPressed != null) | |
ElevatedButton( | |
onPressed: () => onPressed?.call(context), | |
child: const Text('Press me'), | |
) | |
], | |
), | |
), | |
); | |
} | |
} | |
class ScreenWithTabs extends StatefulWidget { | |
final Widget child; | |
const ScreenWithTabs({ | |
Key? key, | |
required this.child, | |
}) : super(key: key); | |
@override | |
State<ScreenWithTabs> createState() => _ScreenWithTabsState(); | |
} | |
class _ScreenWithTabsState extends State<ScreenWithTabs> | |
with SingleTickerProviderStateMixin { | |
late final TabController _tabController; | |
@override | |
void initState() { | |
super.initState(); | |
_tabController = TabController(length: 2, vsync: this, initialIndex: 0); | |
} | |
@override | |
void didChangeDependencies() { | |
_updateSelectedIndex(); | |
super.didChangeDependencies(); | |
} | |
@override | |
void didUpdateWidget(ScreenWithTabs oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
_updateSelectedIndex(); | |
} | |
@override | |
void dispose() { | |
_tabController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Column( | |
mainAxisAlignment: MainAxisAlignment.start, | |
children: [ | |
TabBar( | |
controller: _tabController, | |
onTap: _handleTabSelected, | |
labelColor: Theme.of(context).primaryColor, | |
tabs: const [ | |
Tab(icon: Icon(Icons.tab), text: 'B1'), | |
Tab(icon: Icon(Icons.tab), text: 'B2'), | |
], | |
), | |
Expanded( | |
child: AnimatedSwitcher( | |
duration: const Duration(milliseconds: 300), | |
child: widget.child, | |
), | |
), | |
], | |
), | |
); | |
} | |
// TODO find a generic way | |
void _updateSelectedIndex() { | |
final route = RouteState.of(context); | |
final activeChild = route.activeChild; | |
if (activeChild != null) { | |
if (activeChild.path == '2') _tabController.index = 1; | |
} | |
_tabController.index = 0; | |
} | |
// TODO find a generic way | |
void _handleTabSelected(int index) { | |
switch (index) { | |
case 1: | |
RouteState.of(context).goTo('2'); | |
break; | |
default: | |
RouteState.of(context).goTo('1'); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment