Bottom Navigation and Tabs
Bottom navigation and tab layouts are two related but distinct patterns for letting users switch between top-level sections of an app. Bottom navigation, built with Material 3's NavigationBar and NavigationBarItem, is meant for 3-5 primary destinations that are always visible at the bottom of the screen and typically each have their own back stack. Tabs, built with TabRow and Tab (often paired with a swipeable HorizontalPager), are meant for switching between closely related views of the same content, like different filters on one screen, and usually share a single scroll or state context rather than separate back stacks.
Cricket analogy: Bottom navigation is like the scoreboard's fixed panel showing Batting, Bowling, and Fielding stats always visible, while tabs are like switching between 'Powerplay' and 'Death Overs' views of the same innings you're already watching.
Building a NavigationBar
A NavigationBar composable holds a row of NavigationBarItems, each with an icon, a label, and a selected boolean tied to the current destination. It's typically placed in a Scaffold's bottomBar slot so it persists across navigation within the Scaffold's content area, and each item's onClick triggers navigation to its associated route.
Cricket analogy: A NavigationBar with icons for Live, Scores, and News is like a team's dugout board listing player roles, where tapping a NavigationBarItem for 'Scores' is like a substitute walking onto the field for their assigned position.
sealed class BottomDestination(val route: String, val label: String, val icon: ImageVector) {
object Home : BottomDestination("home", "Home", Icons.Filled.Home)
object Search : BottomDestination("search", "Search", Icons.Filled.Search)
object Profile : BottomDestination("profile", "Profile", Icons.Filled.Person)
}
val bottomDestinations = listOf(
BottomDestination.Home, BottomDestination.Search, BottomDestination.Profile
)
@Composable
fun MainScaffold(navController: NavHostController) {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = backStackEntry?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
bottomDestinations.forEach { destination ->
NavigationBarItem(
selected = currentRoute == destination.route,
onClick = {
navController.navigate(destination.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(destination.icon, contentDescription = destination.label) },
label = { Text(destination.label) }
)
}
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = BottomDestination.Home.route,
modifier = Modifier.padding(innerPadding)
) {
composable(BottomDestination.Home.route) { HomeScreen() }
composable(BottomDestination.Search.route) { SearchScreen() }
composable(BottomDestination.Profile.route) { ProfileScreen() }
}
}
}Tracking the current route
navController.currentBackStackEntryAsState() exposes the current back stack entry as observable Compose State, so reading .destination?.route inside the composable automatically recomposes the NavigationBar's selection highlight whenever the active destination changes — no manual listener wiring required, unlike the older View-based addOnDestinationChangedListener approach.
Cricket analogy: navController.currentBackStackEntryAsState() is like a stadium's automatic scoreboard updating the 'on strike' batsman indicator the instant a run is taken, with no umpire needing to manually signal the change.
The popUpTo/saveState/launchSingleTop/restoreState combination shown above is the standard Google-recommended recipe for bottom navigation: it ensures repeated taps on the same tab don't stack duplicate copies, and switching between tabs preserves each tab's own scroll position and back stack rather than resetting it.
TabRow for content-level tabs
For switching between views of the same content — say, 'Popular' vs 'Recent' posts on one screen — TabRow with a selectedTabIndex and a set of Tab composables is more appropriate than a second NavigationBar. Pairing TabRow with HorizontalPager (from androidx.compose.foundation.pager) lets users both tap a tab and swipe between pages, keeping the selected index and the pager's current page synchronized via a shared PagerState.
Cricket analogy: TabRow with 'Popular' and 'Recent' posts is like switching between a match's 'Scorecard' and 'Commentary' tabs on the same live game page, both views of the identical match rather than separate matches.
Do not nest a NavigationBar inside content that scrolls or changes per tab in a way that creates a second NavController per tab unless you specifically need isolated back stacks — for simple content switching, plain Compose state (selectedTabIndex) with TabRow/HorizontalPager is simpler and avoids unnecessary navigation-graph complexity.
- NavigationBar + NavigationBarItem implement Material 3 bottom navigation for 3-5 top-level, independently-back-stacked destinations.
- currentBackStackEntryAsState() gives reactive access to the current route for driving selection highlighting.
- The popUpTo(startDestinationId) { saveState = true } + launchSingleTop + restoreState combination is the standard recipe to avoid duplicate stacking and preserve per-tab state.
- TabRow + Tab (optionally paired with HorizontalPager) suit switching between views of the same content rather than fully independent destinations.
- PagerState synchronizes a TabRow's selected index with a HorizontalPager's current page.
- Prefer plain Compose state over a full navigation graph for simple, non-back-stacked tab content switching.
Practice what you learned
1. What is the main difference in intended use between NavigationBar and TabRow in Compose?
2. What does navController.currentBackStackEntryAsState() provide?
3. In the standard bottom navigation recipe, what does `saveState = true` on popUpTo accomplish?
4. What Compose component is typically paired with TabRow to allow swiping between tab contents?
5. For simple tab-based content switching within a single screen (not distinct app sections), what is often the simpler alternative to a full nested navigation graph?
Was this page helpful?
You May Also Like
Navigation Compose Basics
An introduction to the Navigation component's Compose integration — NavController, NavHost, and route-based destinations for moving between screens.
Nested Navigation and the Back Stack
How to group related destinations into nested navigation graphs, control back stack behavior with popUpTo, and manage multi-flow apps cleanly.
State Hoisting
Learn the Compose pattern of moving state up to a caller so composables become stateless, reusable, and easier to test.
Layouts: Row, Column, and Box
Row, Column, and Box are Compose's three foundational layout composables for arranging children horizontally, vertically, or stacked, forming the basis of nearly every screen.