6

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.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
Saehun Sean Oh
  • 2,103
  • 1
  • 21
  • 41

3 Answers3

7

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)
Shahab Rauf
  • 3,651
  • 29
  • 38
4

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).

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • Not using `saveState` fixed the issue :), right now I only have simple button and a text, but later I want to add `LazyColumn` for list of items. If I set `saveState = false` wouldn't it forget all the state and scroll positions will be reset as it recomposes? Thank you! – Saehun Sean Oh Aug 24 '21 at 03:34
  • @SaehunSeanOh `saveState = false` discards views you're removing from backstack, for those scroll position will be lost. Your current view won't lost anything. Does it work without replacing `popUpTo` destination? – Phil Dukhov Aug 24 '21 at 03:51
  • 1
    I've made another answer below to include code of how I was able to achieve what I wanted. Thank you – Saehun Sean Oh Aug 24 '21 at 07:32
  • it only works if you want to remove only previous screen from the backstack, what about if I want to remove 2 last screens from the stack before opening the 3 one? We have to set the screen id as we can with navigation component and graph.xml. Basically if I didn't reach 3 then I have to keep last 2 screens and clear them from stack only if I enter 3 screen – user924 Dec 02 '22 at 13:55
0

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.

Saehun Sean Oh
  • 2,103
  • 1
  • 21
  • 41
  • Okay. As I'm looking at this more, I realize how weird this is. so that `popUpTo(item.route)` is really equivalent to `popUpTo(Screen.Home.name)`. I realized it navigates to Home screen, and (should) immediately pops it up to its own, because of `inclusive = true`? Then why do I still have Home? – Saehun Sean Oh Aug 24 '21 at 18:23