android – Jetpack Compose Navigation: How to handle backstack in multi-graph app?

I have two graphs in my app. One is AuthNavGraph which contains auth-related screens like login and signup, and the other is MainNavGraphwhich contains BottomNavigationViewwith top-level destinations. Here are my destinations :

interface RubiBrandsNavigationDestination {

    val route: String
}

sealed class Destination(
    override val route: String,
    val arguments: List<NamedNavArgument> = emptyList()
) : RubiBrandsNavigationDestination {

    object LoginDestination : Destination(
        route = LOGIN_SCREEN_ROUTE,
    )

    object SignupDestination : Destination(
        route = SIGNUP_SCREEN_ROUTE,
    )

    object ForgotPasswordDestination : Destination(
        route = FORGOT_PASSWORD_SCREEN_ROUTE
    )

    object ResetPasswordDestination : Destination(
        route = RESET_PASSWORD_SCREEN_ROUTE
    )

}

sealed class TopLevelDestination(
    override val route: String,
    @DrawableRes val selectedIcon: Int,
    @DrawableRes val unselectedIcon: Int,
    @StringRes val destinationLabel: Int,
    arguments: List<NamedNavArgument> = emptyList(),
) : Destination(
    route = route,
    arguments = arguments
) {
    object HomeDestination : TopLevelDestination(
        route = HOME_SCREEN_ROUTE,
        selectedIcon = com.path.rubi.brands.core.ui.R.drawable.ic_selected_home,
        unselectedIcon = com.path.rubi.brands.core.ui.R.drawable.ic_unselected_home,
        destinationLabel = com.path.rubi.brands.core.ui.R.string.home,
    )

    object OrdersDestination : TopLevelDestination(
        route = ORDERS_SCREEN_ROUTE,
        selectedIcon = com.path.rubi.brands.core.ui.R.drawable.ic_selected_orders,
        unselectedIcon = com.path.rubi.brands.core.ui.R.drawable.ic_unselected_orders,
        destinationLabel = com.path.rubi.brands.core.ui.R.string.orders,
    )

    object ProductsDestination : TopLevelDestination(
        route = PRODUCTS_SCREEN_ROUTE,
        selectedIcon = com.path.rubi.brands.core.ui.R.drawable.ic_selected_products,
        unselectedIcon = com.path.rubi.brands.core.ui.R.drawable.ic_unselected_products,
        destinationLabel = com.path.rubi.brands.core.ui.R.string.products,
    )

    object AccountDestination : TopLevelDestination(
        route = ACCOUNT_SCREEN_ROUTE,
        selectedIcon = com.path.rubi.brands.core.ui.R.drawable.ic_selected_account,
        unselectedIcon = com.path.rubi.brands.core.ui.R.drawable.ic_unselected_account,
        destinationLabel = com.path.rubi.brands.core.ui.R.string.account,
    )
}

Here are the routes of the destinations :

object Routes {

    //routes

    //auth flow
    const val LOGIN_SCREEN_ROUTE = "login_screen_route"

    const val SIGNUP_SCREEN_ROUTE = "signup_screen_route"

    const val FORGOT_PASSWORD_SCREEN_ROUTE = "forgot_password_screen_route"

    const val RESET_PASSWORD_SCREEN_ROUTE = "reset_password_screen_route"


    //main flow
    const val HOME_SCREEN_ROUTE = "home_screen_route"

    const val ORDERS_SCREEN_ROUTE = "orders_screen_route"

    const val PRODUCTS_SCREEN_ROUTE = "products_screen_route"

    const val ACCOUNT_SCREEN_ROUTE = "account_screen_route"

    //graphs
    const val ROOT_GRAPH = "root_graph"

    const val AUTH_GRAPH = "auth_graph"

    const val MAIN_GRAPH = "main_graph"
}

Here is also my navigation setup:


@Composable
fun RubiBrandsApp(startDestination: String) {
    RubiBrandsTheme {
        val navController = rememberAnimatedNavController()
        val navBackStackEntry by navController.currentBackStackEntryAsState()

        val topLevelDestinations = listOf(
            TopLevelDestination.HomeDestination.route,
            TopLevelDestination.OrdersDestination.route,
            TopLevelDestination.ProductsDestination.route,
            TopLevelDestination.AccountDestination.route
        )

        val isNavBarVisible = navBackStackEntry?.destination?.route in topLevelDestinations

        Scaffold(
            bottomBar = {
                if (isNavBarVisible) {
                    RubiBrandsBottomNavigationView(
                        navController = navController,
                    )
                }
            }
        ) { padding ->
            //navigation graph
            Box(
                modifier = Modifier
                    .padding(padding)
            ) {
                NavGraph(
                    navController = navController,
                    startDestination = startDestination,
                )
            }
        }
    }
}

//root nav graph

@Composable
fun NavGraph(
    navController: NavHostController,
    modifier: Modifier = Modifier,
    startDestination: String
) {
    AnimatedNavHost(
        navController = navController,
        startDestination = startDestination,
    ) {

            //auth graph
            authNavGraph(navController = navController, modifier = modifier)

            //main graph
            mainNavGraph(navController = navController)

        }
}

//auth nav-graph

fun NavGraphBuilder.authNavGraph(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    navigation(
        startDestination = Destination.LoginDestination.route,
        route = AUTH_GRAPH
    ) {
        addLoginScreen(navController = navController, modifier = modifier)

        addSignupScreen(navController = navController)

        addResetPasswordScreen(navController = navController)

        addForgotPasswordScreen(navController = navController)
    }
}

fun NavGraphBuilder.addLoginScreen(
    navController: NavController,
    modifier: Modifier = Modifier
) {
    composable(
        route = Destination.LoginDestination.route,
        arguments = Destination.LoginDestination.arguments,
        enterTransition = {
            scaleIn(
                animationSpec = spring()
            )
        },
        exitTransition = {
            slideOutHorizontally(targetOffsetX = { -5000 }, animationSpec = tween(300))
        },
        popEnterTransition = {
            slideInHorizontally(initialOffsetX = { -5000 }, animationSpec = tween(600))
        },
        popExitTransition = {
            slideOutHorizontally(targetOffsetX = { 1000 }, animationSpec = tween(700))
        }
    ) {
        //login screen
        LoginScreen(
            modifier = modifier,
            navigateToHomeScreen = {
             when the users logged in successfully I'm navigating the user to the main-graph
                navController.navigate(MAIN_GRAPH)
            },
            navigateToForgotPassword = {
                navController.navigate(Destination.ForgotPasswordDestination.route)
            },
            navigateToSignupScreen = {
                navController.navigate(Destination.SignupDestination.route)
            }
        )

    }
}

//main nav-graph

fun NavGraphBuilder.mainNavGraph(
    navController: NavController
) {
    navigation(
        startDestination = TopLevelDestination.HomeDestination.route,
        route = MAIN_GRAPH,
    ) {

        addHomeScreen(navController)

        addOrdersScreen(navController)

        addProductsScreen(navController)

        addAccountScreen(navController)
    }
}

fun NavGraphBuilder.addHomeScreen(
    navController: NavController
) {
    composable(
        route = TopLevelDestination.HomeDestination.route,
        arguments = TopLevelDestination.HomeDestination.arguments,

    ) {
        //home screen
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Home Screen", textAlign = TextAlign.Center)
        }
    }
}

// other screens

Lastly my BottomNavigationView:

@Composable
fun RubiBrandsBottomNavigationView(
    modifier: Modifier = Modifier,
    navController: NavHostController,
) {

    val topLevelDestinations = listOf(
        TopLevelDestination.HomeDestination,
        TopLevelDestination.OrdersDestination,
        TopLevelDestination.ProductsDestination,
        TopLevelDestination.AccountDestination
    )

    val backStackEntry = navController.currentBackStackEntryAsState()

    val selectedRoute = backStackEntry.value?.destination?.route

    val routes = remember { topLevelDestinations.map { it.route } }

    BottomNavigation(
        modifier = modifier,
        backgroundColor = MaterialTheme.colors.primaryVariant,
    ) {
        topLevelDestinations.forEach { destination ->
            val selected = destination.route == selectedRoute
            BottomNavigationItem(
                selected = destination.route == selectedRoute,
                onClick = {
                        navController.navigate(destination.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
                            // reselecting the same item
                            launchSingleTop = true
                            // Restore state when reselecting a previously selected item
                            restoreState = true
                        }
                          },
                label = {
                    Text(
                        text = stringResource(id = destination.destinationLabel),
                        softWrap = false,
                        style = MaterialTheme.typography.caption.copy(fontSize = 13.sp)
                    )
                },
                icon = {
                    val icon = if (selected) {
                        destination.selectedIcon
                    } else {
                        destination.unselectedIcon
                    }
                    Icon(
                        painter = painterResource(icon),
                        contentDescription = null,
                        modifier = Modifier
                            .offset(y = (-2).dp)
                            .weight(1f)
                    )
                }
            )
        }
    }
}

My problem is BottomNavigationView multi-backstack feature is not working as intended. If I set the start destination as MAIN_GRAPH which contains my top-level destinations and BottomNavigationView it works as intended but if I set the start destination to AUTH_GRAPH then navigate to MAIN_GRAPH after logging in it causes a problem.

Here is the case :

  1. I set my start destination to MAIN_GRAPH which works as expected:

  1. I set my start destination to AUTH_GRAPH and then navigate to MAIN_GRAPH :

To prevent this I have popup the destination to MAIN_GRAPH when the user navigates from loginScreen to MAIN_GRAPH :


  //login screen
        LoginScreen(
            modifier = modifier,
            navigateToHomeScreen = {
                navController.navigate(MAIN_GRAPH){
                    popUpTo(MAIN_GRAPH){
                     
                    }
                }
            })

but the result is still the same :

My navigation compose version is 2.4.2.

What should I do?

Leave a Comment