Mastering Nested Navigation in Jetpack Compose: A Comprehensive Guide
Introduction
Jetpack Compose has revolutionized Android UI development with its declarative approach, offering developers a more intuitive way to build complex interfaces. However, as apps grow in complexity, so do their navigation requirements. A common challenge in modern Android apps is managing multiple navigation stacks simultaneously, especially when using a bottom navigation bar. This is where nested navigation in Jetpack Compose comes into play.
In this guide, we’ll explore how to implement nested navigation in Jetpack Compose, ensuring that each tab in your bottom navigation has its own independent navigation stack. We’ll also cover how to manage root and leaf nodes in the navigation hierarchy and how to integrate bottom sheets into your navigation flow.
The Challenge of Nested Navigation
Imagine an app with multiple bottom navigation tabs, where each tab has its own set of screens. When a user switches between tabs, you want to preserve the state of each tab’s navigation stack. For example, if a user navigates deep into the settings tab and then switches to the profile tab, returning to the settings tab should bring them back to where they left off, not to the root of the settings tab.
Get Ilker Ulusoy’s stories in your inbox
Join Medium for free to get updates from this writer.
To achieve this, you need to implement nested navigation, where each tab has its own NavController and NavHost, managing its navigation independently.
Step 1: Setting Up the Navigation Architecture
The first step is to set up a basic navigation structure with multiple tabs, each with its own navigation stack. Here’s how to start:
Defining the Navigation Components
First, define your navigation destinations and the corresponding screens. Let’s assume you have four tabs: Home, Settings, Profile, and Notifications.
sealed class Screen(val route: String) {
object Home : Screen("home")
object Settings : Screen("settings")
object Profile : Screen("profile")
object Notifications : Screen("notifications")
data class Detail(val id: String) : Screen("detail/{id}") {
fun createRoute(id: String) = "detail/$id"
}
}Setting Up the Bottom Navigation
Next, set up your bottom navigation bar and define a NavHost for each tab.
@Composable
fun MainScreen() {
val homeNavController = rememberNavController()
val settingsNavController = rememberNavController()
val profileNavController = rememberNavController()
val notificationsNavController = rememberNavController()
var selectedTab by rememberSaveable { mutableStateOf(Screen.Home.route) }
Scaffold(
bottomBar = {
BottomNavigation {
listOf(Screen.Home, Screen.Settings, Screen.Profile, Screen.Notifications).forEach { screen ->
BottomNavigationItem(
selected = selectedTab == screen.route,
onClick = { selectedTab = screen.route },
icon = { Icon(imageVector = Icons.Default.Home, contentDescription = null) },
label = { Text(screen.route.capitalize()) }
)
}
}
}
) { innerPadding ->
Box(Modifier.padding(innerPadding)) {
when (selectedTab) {
Screen.Home.route -> {
NavHost(navController = homeNavController, startDestination = Screen.Home.route) {
composable(Screen.Home.route) { HomeScreen(homeNavController) }
composable(Screen.Detail("0").route) { backStackEntry ->
DetailScreen(homeNavController, backStackEntry.arguments?.getString("id") ?: "")
}
}
}
Screen.Settings.route -> {
NavHost(navController = settingsNavController, startDestination = Screen.Settings.route) {
composable(Screen.Settings.route) { SettingsScreen(settingsNavController) }
}
}
Screen.Profile.route -> {
NavHost(navController = profileNavController, startDestination = Screen.Profile.route) {
composable(Screen.Profile.route) { ProfileScreen(profileNavController) }
}
}
Screen.Notifications.route -> {
NavHost(navController = notificationsNavController, startDestination = Screen.Notifications.route) {
composable(Screen.Notifications.route) { NotificationsScreen(notificationsNavController) }
}
}
}
}
}
}In this setup, each tab’s navigation stack is managed independently by its own NavController.
Step 2: Handling Root and Leaf Nodes
A common requirement is to show a back button only on screens that are not the root of their navigation stack. To determine if a screen is a root or a leaf, you can check the size of the back stack:
@Composable
fun isRoot(navController: NavController): Boolean {
return navController.backQueue.size <= 2 // The first entry is the NavGraph itself
}
@Composable
fun PageWrapper(
navController: NavController,
title: String,
content: @Composable () -> Unit
) {
val isRoot = isRoot(navController)
Scaffold(
topBar = {
if (!isRoot) {
TopAppBar(
title = { Text(title) },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back")
}
}
)
} else {
TopAppBar(title = { Text(title) })
}
}
) {
content()
}
}Step 3: Adding Bottom Sheets
In some cases, you may want to open certain pages as bottom sheets. Jetpack Compose makes this easy with ModalBottomSheetLayout:
@Composable
fun SomePageWithSheet(navController: NavController) {
val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val coroutineScope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
BottomSheetContent()
}
) {
PageWrapper(
navController = navController,
title = "Some Page"
) {
Column {
Text("This is some page content.")
Button(onClick = {
coroutineScope.launch {
sheetState.show()
}
}) {
Text("Open Bottom Sheet")
}
}
}
}
}Conclusion
Nested navigation in Jetpack Compose allows you to create a seamless and intuitive user experience by managing multiple independent navigation stacks. By leveraging NavController and NavHost for each tab, you ensure that users can navigate freely without losing their place within the app.
With the addition of root and leaf detection, you can fine-tune the user interface to show or hide navigation controls as needed. Integrating bottom sheets further enhances the flexibility and dynamism of your app.
Mastering nested navigation is a key step in building modern, complex Android applications with Jetpack Compose, and it provides a solid foundation for handling even more intricate navigation scenarios as your app evolves.
Happy coding! 🎉




