Is there a proper example of type-safe navigation with nested graphs where one of the graphs includes a NavigationBar with its own screens, and an auth/splash/onboarding screen is removed from the back stack using inclusive = true, all without duplicating rememberNavController/NavHost?
@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { IncomeTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { NavigationHost() } } } } } @Serializable sealed class Graph { @Serializable object OnboardingGraph : Graph() @Serializable object MainGraph : Graph() } @Serializable sealed class OnboardingGraph { @Serializable object OnboardingScreen : OnboardingGraph() } @Serializable sealed class MainGraph { @Serializable object Home : MainGraph() @Serializable object Notification : MainGraph() @Serializable object History : MainGraph() @Serializable object Settings : MainGraph() } enum class TopLevelRoute( val route: Any, @StringRes val label: Int, @DrawableRes val iconFill: Int ) { HOME( MainGraph.Home, R.string.tab_home, R.drawable.baseline_home_24 ), NOTIFICATION( MainGraph.Notification, R.string.tab_notification, R.drawable.baseline_notifications_24 ), HISTORY( MainGraph.History, R.string.tab_history, R.drawable.baseline_history_edu_24 ), SETTINGS( MainGraph.Settings, R.string.tab_settings, R.drawable.baseline_settings_24 ); } @Composable fun BottomNavigationBar( navController: NavHostController, currentDestination: NavDestination? ) { NavigationBar { TopLevelRoute.entries.forEach { navigationItem -> NavigationBarItem( selected = currentDestination?.hierarchy?.any { it.hasRoute(navigationItem.route::class) } == true, label = { Text(text = stringResource(navigationItem.label)) }, icon = { Icon( painterResource(navigationItem.iconFill), contentDescription = stringResource(navigationItem.label) ) }, onClick = { navController.navigate(navigationItem.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } ) } } } @Composable private fun NavigationHost() { val navController = rememberNavController() NavHost( navController = navController, startDestination = Graph.OnboardingGraph, enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None } ) { onboardingNavGraph(navController) composable<MainGraph.Home> { MainScreenUI(navController = navController) } } } fun NavGraphBuilder.onboardingNavGraph( navController: NavController ) { navigation<Graph.OnboardingGraph>(startDestination = OnboardingGraph.OnboardingScreen) { composable<OnboardingGraph.OnboardingScreen> { OnboardingScreen( onFinished = { navController.navigate(MainGraph.Home) { popUpTo(OnboardingGraph.OnboardingScreen) { inclusive = true } } } ) } } } @Composable fun MainScreenUI( navController: NavHostController ) { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { BottomNavigationBar( navController = navController, currentDestination = currentDestination ) } ) { paddingValues -> NavHost( modifier = Modifier.padding(paddingValues), navController = navController, startDestination = Graph.MainGraph, enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None } ) { mainNavGraph(navController) } } } fun NavGraphBuilder.mainNavGraph( navController: NavController ) { navigation<Graph.MainGraph>(startDestination = MainGraph.Home) { composable<MainGraph.Home> { HomeScreen { /* TODO */ } } composable<MainGraph.Notification> { NotificationScreen { /* TODO */ } } composable<MainGraph.History> { HistoryScreen { /* TODO */ } } composable<MainGraph.Settings> { SettingsScreen { /* TODO */ } } } } Also tried using a single NavHost with two nested graphs, but calling:
navController.navigate(MainGraph.Home) { popUpTo(OnboardingGraph.OnboardingScreen) { inclusive = true } } results in the error: "Ignoring popBackStack to destination XXXXXX as it was not found on the current back stack", as well as screen flickering when navigating to the same screen again. The error occurs because, when tapping on a bottom navigation item, the popUpTo method is called with the start destination ID, which is determined using navController.graph.findStartDestination().id. However, the start destination of the graph remains the onboarding screen, which was already removed from the back stack after it was completed. As a result, an attempt is made to "pop up" to a screen that no longer exists in the stack. (In the case of attempting to use two graph builders with a single NavHost) At the moment, I see a solution by setting the MainGraph\Home screen as the destination in the bottom navigation instead of using findStartDestination(). However, I'm not sure if this is the best practice.
@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { IncomeTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { MainScreenUI() } } } } } @Serializable sealed class Graph { @Serializable object OnboardingGraph : Graph() @Serializable object MainGraph : Graph() } @Serializable sealed class OnboardingGraph { @Serializable object OnboardingScreen : OnboardingGraph() } @Serializable sealed class MainGraph { @Serializable object Home : MainGraph() @Serializable object Notification : MainGraph() @Serializable object History : MainGraph() @Serializable object Settings : MainGraph() } enum class TopLevelRoute( val route: Any, @StringRes val label: Int, @DrawableRes val iconFill: Int ) { HOME( MainGraph.Home, R.string.tab_home, R.drawable.baseline_home_24 ), NOTIFICATION( MainGraph.Notification, R.string.tab_notification, R.drawable.baseline_notifications_24 ), HISTORY( MainGraph.History, R.string.tab_history, R.drawable.baseline_history_edu_24 ), SETTINGS( MainGraph.Settings, R.string.tab_settings, R.drawable.baseline_settings_24 ); } @Composable fun MainScreenUI() { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination Scaffold( modifier = Modifier.fillMaxSize(), bottomBar = { // distance check to exclude non-incoming screens BottomNavigationBar( navController = navController, currentDestination = currentDestination ) } ) { paddingValues -> NavHost( modifier = Modifier.padding(paddingValues), navController = navController, startDestination = Graph.OnboardingGraph, enterTransition = { EnterTransition.None }, exitTransition = { ExitTransition.None } ) { onboardingNavGraph(navController) mainNavGraph(navController) } } } @Composable fun BottomNavigationBar( navController: NavHostController, currentDestination: NavDestination? ) { NavigationBar { TopLevelRoute.entries.forEach { navigationItem -> NavigationBarItem( selected = currentDestination?.hierarchy?.any { it.hasRoute(navigationItem.route::class) } == true, label = { Text(text = stringResource(navigationItem.label)) }, icon = { Icon( painterResource(navigationItem.iconFill), contentDescription = stringResource(navigationItem.label) ) }, onClick = { navController.navigate(navigationItem.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } ) } } } fun NavGraphBuilder.onboardingNavGraph( navController: NavController ) { navigation<Graph.OnboardingGraph>(startDestination = OnboardingGraph.OnboardingScreen) { composable<OnboardingGraph.OnboardingScreen> { OnboardingScreen( onFinished = { navController.navigate(MainGraph.Home) { popUpTo(OnboardingGraph.OnboardingScreen) { inclusive = true } } } ) } } } fun NavGraphBuilder.mainNavGraph( navController: NavController ) { navigation<Graph.MainGraph>(startDestination = MainGraph.Home) { composable<MainGraph.Home> { HomeScreen { /* TODO */ } } composable<MainGraph.Notification> { NotificationScreen { /* TODO */ } } composable<MainGraph.History> { HistoryScreen { /* TODO */ } } composable<MainGraph.Settings> { SettingsScreen { /* TODO */ } } } }