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