Skip to content

Instantly share code, notes, and snippets.

@daanporon
Created June 22, 2022 07:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daanporon/2356fcf269cc5cd9156f60be72ba3b27 to your computer and use it in GitHub Desktop.
Save daanporon/2356fcf269cc5cd9156f60be72ba3b27 to your computer and use it in GitHub Desktop.
Go example with subnavigation and tabbed navigation
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