Managing multiple tabs, each with its own navigation history, can feel daunting. In this post, we’ll explore how to use go_router
to set up nested navigation in Flutter—complete with a persistent BottomNavigationBar—while still being able to navigate into detail screens without losing the bottom bar.
1. Why Nested Navigation?
In many apps (e.g., music or podcast apps), each bottom-tab (Home, Discover, Library, Profile, etc.) needs to maintain its own navigation history. When a user switches tabs, they should return to the previous screen within that tab, not reset to the root screen.
Key points:
- Preserve each tab’s state when switching.
- Allow deeper routes (Detail screens) inside each tab.
- Keep the bottom bar visible even on detail pages.
2. Setting Up a StatefulShellRoute.indexedStack
go_router
offers a StatefulShellRoute
that behaves like an IndexedStack
. Each tab corresponds to a StatefulShellBranch
:
final router = GoRouter( routes: [ StatefulShellRoute.indexedStack( builder: (context, state, navShell) => Scaffold( body: navShell, bottomNavigationBar: BottomNavigationBar(...), ), branches: [ // Branch 1: Home StatefulShellBranch( routes: [ GoRoute( path: '/home', builder: (context, state) => HomePage(), routes: [ GoRoute( path: 'podcast/:podcastId', builder: (context, state) => PodcastDetailPage(), ), ], ), ], ), // Branch 2: Discover StatefulShellBranch( routes: [ GoRoute( path: '/discover', builder: (context, state) => DiscoverPage(), routes: [ GoRoute( path: 'podcast/:podcastId', builder: (context, state) => PodcastDetailPage(), ), ], ), ], ), // ...Other branches ], ), ], );
- Each branch has its own sub-routes.
- Navigating to
'/home/podcast/123'
will push a detail page within the Home branch, so your bottom bar remains.
3. Making Detail Routes “Shared” Without Repetition
If you have the same detail screen (e.g., PodcastDetailPage
) in multiple tabs, consider a helper function to generate shared routes:
List<GoRoute> buildPodcastRoutes(String branchName) => [ GoRoute( name: '${branchName}PodcastDetail', path: 'podcast/:podcastId', builder: (context, state) => PodcastDetailPage(), ), ]; // Then in each branch: GoRoute( path: '/home', routes: [ ...buildPodcastRoutes('home'), ], builder: (context, state) => HomePage(), ),
This way, you “define once,” but attach them to each branch. Each detail route name is unique (homePodcastDetail
, discoverPodcastDetail
, etc.), allowing goNamed
without clashing.
4. Navigating to Detail from Shared Widgets
You might have shared widgets (like PodcastCard
) that shouldn’t hard-code which tab’s detail route to use. One clean approach is Inversion of Control:
- Parent (e.g.,
HomePage
) passes a callback toPodcastCard
. -
PodcastCard
just callsonTap?.call()
without knowing the route name.
class PodcastCard extends StatelessWidget { final String podcastId; final VoidCallback onTap; // ... @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: ... ); } } class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return PodcastCard( podcastId: '123', onTap: () => context.goNamed('homePodcastDetail', params: {'podcastId': '123'}), ); } }
This keeps routing logic in the page (which knows it’s the Home tab), not in the shared widget.
5. Best Practices at a Glance
- Use
StatefulShellRoute.indexedStack
for bottom-tabs, ensuring each tab has a separate route branch and persistent state. - Keep detail routes as children of each tab’s branch, so the bottom bar never disappears.
- Avoid repeating route definitions with helper methods that generate shared detail routes for each branch.
- Leverage named routes (
goNamed
/pushNamed
) to avoid hard-coding paths. - Separate your UI from navigation logic by passing callbacks or using a provider/DI approach.
6. Wrapping Up
With go_router
, setting up nested navigation for multiple tabs is remarkably straightforward—once you know how to structure your routes! The StatefulShellRoute
keeps each tab’s history intact, and sub-routes let you push detail pages without losing the bottom bar. Combine these techniques with consistent naming for a scalable, clean navigation system in Flutter.
Happy routing!
Top comments (1)
Well done!