I’m trying to get nested navigation to work with ShellRoute
in go_router 5.0.
As the example below shows, I can correctly navigate to the (nested) product page when clicking on a product.
But if I switch to the cart tab and back to products, I'm taken back to the (root) products page rather than the (nested) product page:
This is how I've setup my router:
enum AppRoute {
products,
product,
cart,
account,
}
final goRouter = GoRouter(
initialLocation: '/products',
navigatorKey: _rootNavigatorKey,
debugLogDiagnostics: true,
routes: [
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (context, state, child) {
return ScaffoldWithBottomNavBar(child: child);
},
routes: [
// Products
GoRoute(
path: '/products',
name: AppRoute.products.name,
redirect: (context, state) {
print(state.toString());
return null;
},
pageBuilder: (context, state) => NoTransitionPage(
key: state.pageKey,
restorationId: state.pageKey.value,
child: const ProductsListScreen(),
),
routes: [
GoRoute(
path: ':id',
name: AppRoute.product.name,
pageBuilder: (context, state) {
final productId = state.params['id']!;
// TODO: Cupertino slide transition
return MaterialPage(
key: state.pageKey,
restorationId: state.pageKey.value,
child: ProductScreen(productId: productId),
);
},
),
],
),
// Shopping Cart
GoRoute(
path: '/cart',
name: AppRoute.cart.name,
pageBuilder: (context, state) => NoTransitionPage(
key: state.pageKey,
child: const ShoppingCartScreen(),
),
),
// Account page
GoRoute(
path: '/account',
name: AppRoute.account.name,
pageBuilder: (context, state) => NoTransitionPage(
key: state.pageKey,
child: const AccountScreen(),
),
),
],
),
],
);
This is the build
method of the ScaffoldWithBottomNavBar
widget:
@override
Widget build(BuildContext context) {
return Scaffold(
body: widget.child,
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
unselectedItemColor: Colors.grey,
selectedItemColor: Colors.black87,
currentIndex: _selectedIndex,
items: [
BottomNavigationBarItem(
icon: const Icon(
Icons.list,
),
label: 'Products'.hardcoded,
),
BottomNavigationBarItem(
icon: const ShoppingCartIcon(),
label: 'Cart'.hardcoded,
),
BottomNavigationBarItem(
icon: const Icon(
Icons.account_circle,
),
label: 'Account'.hardcoded,
),
],
onTap: (index) => _tap(context, index),
),
);
}
In the tab button callback, I do this:
void _tap(BuildContext context, int index) {
setState(() => _selectedIndex = index); // used for the highlighted state
// navigate to the target route based on the tab index
if (index == 0) {
context.goNamed(AppRoute.products.name);
} else if (index == 1) {
context.goNamed(AppRoute.cart.name);
} else if (index == 2) {
context.goNamed(AppRoute.account.name);
}
}
I understand why this does not work since I’m telling GoRouter to go to each of the (root) routes inside the ShellRoute
.
Ideally, I'd want a way to switch to a specific tab by index, and have GoRouter "remember" if it was on a nested or top-level route.
Note that unlike TabBar
, BottomNavigationBar
does not have a controller I can use to switch the index.
GoRouter 5.0 is new and there isn't much documentation or examples showing these kind of common use cases.
Anyone knows how to approach this?
Note: one of my ideas was to store the selected product ID as application state somewhere, and use it inside a redirect
function inside the /products
route, but then it becomes complex to know where I should reset that state to null
, and my experiments with NavigationObserver
did not yield the desired result.
Update 28th Jun 2023
GoRouter 7.1.0 added a new StatefulShellRoute
class to support this.
I've written a full tutorial explaining how to use it: