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 (0)