Jetpack Compose Navigation: PopUpTo Screen from Screens that have same route except argument

15.8k views Asked by At

navigation compose version 2.4.0-alpha06

I have a Navigation Drawer using Scaffold and part of the items are dynamically generated by ViewModel.

Example items are

  • Home
  • A
  • B
  • C ...
  • Settings

where A, B, C, ... all share same Screen called Category, with just different arguments passed (e.g. Category/A, Category/B).

Inside my Scaffold

...

val items = viewModel.getDrawerItems()
// This gives something like 
// ["Home", "Category/A", "Category/B", "Category/C", ..., "Settings"] 
// where each String represents "route"

...

val backstackEntry = navController.currentBackStackEntryAsState()
val currentScreen = Screen.fromRoute(
    backstackEntry.value?.destination?.route
)
Log.d("Drawer", "currentScreen: $currentScreen")

items.forEach { item ->
    DrawerItem(
        item = item, 
        isSelected = currentScreen.name == item.route, 
        onItemClick = {
            Log.d("Drawer", "destinationRoute: ${item.route}")
            navController.navigate(item.route)
            scope.launch {
                scaffoldState.drawerState.close()
            }
        }
    )
}

This code works pretty well, except when I visit Home screen, I want to clear all backstack upto Home not inclusive.

I've tried adding NavOptionsBuilder

...

navController.navigate(item.route) {
    popUpTo(currentScreen.name) {
        inclusive = true
        saveState = true
    }
}
...

However, this doesn't work because currentScreen.name will give something like Category/{title} and popUpTo only tries to look up exact match from the backstack, so it doesn't pop anything.

Is there real compose-navigation way to solve this? or should I save the last "title" somewhere in ViewModel and use it?

This tutorial from Google has similar structure, but it just stacks screens so going back from screen A -> B -> A and clicking back will just go back to B -> A, which is not ideal behavior for me.

Thank you in advance.

3

There are 3 answers

4
Phil Dukhov On BEST ANSWER

When you're specifying popUpTo you should pass same item you're navigating to in this case:

navController.navigate(item.route) {
    popUpTo(item.route) {
        inclusive = true
    }
}

Also not sure if you need to specify saveState in this case, it's up to you:

Whether the back stack and the state of all destinations between the current destination and the NavOptionsBuilder.popUpTo ID should be saved for later restoration via NavOptionsBuilder.restoreState or the restoreState attribute using the same NavOptionsBuilder.popUpTo ID (note: this matching ID is true whether inclusive is true or false).

0
Shahab Rauf On

You can make an extension function to serve the popUpTo functionality at all places.

fun NavHostController.navigateWithPopUp(
    toRoute: String,  // route name where you want to navigate
    fromRoute: String // route you want from popUpTo.
) {
    this.navigate(toRoute) {
        popUpTo(fromRoute) {
            inclusive = true // It can be changed to false if you
                             // want to keep your fromRoute exclusive
        }
    }
}

Usage

navController.navigateWithPopUp(Screen.Home.name, Screen.Login.name)
1
Saehun Sean Oh On

Inspired by @Philip Dukhov's answer, I was able to achieve what I wanted.

...

navController.navigate(item.route) {
    // keep backstack until user goes to Home
    if (item.route == Screen.Home.name) {
        popUpTo(item.route) {
            inclusive = true
            saveState = true
        }
    } else {
        // only restoreState for non-home screens
        restoreState = true
    }
...

Unfortunately, if I add launchSingleTop = true, Screen with different argument is not recomposed for some reason, but that's probably another topic.