Bottom Nav with Nav Graphs in Compose

January 6, 2022

In a large app, having a flat organization for your navigation destinations can be hard to maintain.

Organizing navigation destinations using nav graphs can make maintaining your code a lot easier.

Above you see an example with 2 nav graphs with some destinations under each as well as a standalone navigation destination (Destination F).

 

This post will provide an example app with a bottom nav: each bottom nav item will have its own nav graph (and its own backstack) to put destinations in. There are are 2 bottom navigation items (Home and Settings). Each bottom nav item is a nav graph (Home Nav Graph and Settings Nav Graph). The Settings nav graph has a regular (non-bottom-nav) destination, About Screen.

Notice that each bottom nav item (nav graph) maintains its own backstack. As seen in the .gif, we can go from the About screen inside the Settings Nav Graph to the Home screen and back by tapping on the bottom nav.

Home Nav Graph

fun NavGraphBuilder.homeNavGraph() {
    navigation(
        startDestination = Screen.Home.route,
        route = HOME_GRAPH_ROUTE
    ) {
        composable(Screen.Home.route) {
            HomeScreen()
        }
    }
}

Settings Nav Graph

fun NavGraphBuilder.settingsNavGraph(
    navController: NavHostController
) {
    navigation(
        startDestination = Screen.Settings.route,
        route = SETTINGS_GRAPH_ROUTE
    ) {
        composable(Screen.Settings.route) {
            SettingsScreen(navController)
        }
        composable(Screen.About.route) {
            AboutScreen(navController)
        }
    }
}

Nav Host

const val HOME_GRAPH_ROUTE = "home"
const val SETTINGS_GRAPH_ROUTE = "settings"

@Composable
fun Navigation(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = HOME_GRAPH_ROUTE
    ) {
        homeNavGraph()
        settingsNavGraph(navController = navController)
    }
}

Bottom Nav

sealed class BottomNavItem(
    val route: String,
    @StringRes val titleResId: Int,
    val icon: ImageVector
) {
    object Home : BottomNavItem(
        route = HOME_GRAPH_ROUTE,
        titleResId = R.string.screen_title_home,
        icon = Icons.Default.Home
    )

    object Settings : BottomNavItem(
        route = SETTINGS_GRAPH_ROUTE,
        titleResId = R.string.screen_title_settings,
        icon = Icons.Default.Settings
    )
}

@Composable
fun BottomNavigationBar(
    navController: NavController
) {
    val items = listOf(
        BottomNavItem.Home,
        BottomNavItem.Settings
    )

    BottomNavigation {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route
        items.forEach { item ->
            BottomNavigationItem(
                icon = {
                    Icon(
                        imageVector = item.icon,
                        contentDescription = stringResource(id = item.titleResId)
                    )
                },
                label = { Text(text = stringResource(id = item.titleResId)) },
                selected = currentRoute == item.route,
                onClick = {
                    navController.navigate(item.route) {
                        // Pop up to the start destination of the graph to
                        // avoid building up a large stack of destinations
                        // on the back stack as users select items
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        // Avoid multiple copies of the same destination when re-selecting the same item
                        launchSingleTop = true
                        // Restore state when re-selecting a previously selected item
                        restoreState = true
                    }
                }
            )
        }
    }
}

Note that the routes for the Bottom Nav Items are those of the nav graphs (not destination routes themselves). By using the graph routes, you ensure that navigation properly restores your entire graph for that particular tab.

 

Good thing about the above setup is to be able to logically break down your navigation setup into separate files so you can place destinations where they logically belong:

  • BottomNav.kt

  • HomeNavGraph.kt

  • SettingsNavGraph.kt

  • NavGraph.kt

Tying it all together

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Surface(color = MaterialTheme.colors.background) {
                val navController = rememberNavController()
                Scaffold(
                    bottomBar = { BottomNavigationBar(navController) }
                ) {
                    Navigation(navController = navController)
                }
            }
        }
    }
}

You can see full source code for this post here

 
Previous
Previous

StateFlow vs SharedFlow in Compose

Next
Next

Intercept back press in Jetpack Compose