Compose Navigation : Passing Arguments in Back Stack
Asked Answered
C

3

12

I have tried all possible way looks like not getting a solution to this.

I am using an all compose app with compose screens and using compose navigation to navigate between this screen, I have a scenario where I am navigating further in this manner

Screen A > Screen B1 >..> Screen BN > Screen C

Now After I have done with the function on Screen C, I want to pop back to Screen A but this time with an optional argument for eg: Success

I have done this to navigate to A:

val route = "ScreenA?arg={arg}"

// navigating like this
navController.navigate("ScreenA")


// handling
composable(route, arguments = listOf(navArgument("arg") { nullable = true })){
  ScreenA()
}

Now for popping back to ScreenA from where ever I am I have done this:

navController.popBackStack(
                route = route,
                inclusive = false
            )

I want to send argument now while popping back. I have tried adding route as

 ScreenA?arg=success

Which Doesn't work as inside popBackStack function, it checks the hashcode of the route

popBackStack(createRoute(route).hashCode(), inclusive, saveState)

in this case my navigation fails

I have tried setting argument to back stack entry also but as it can be N Screen in between it wont work. Need to understand where I am doing wrong or if theres a way to do it?

Congregational answered 3/8, 2022 at 11:35 Comment(0)
C
6

Try setting value to previousBackStackEntry like

navController.previousBackStackEntry?.savedStateHandle?.set(
                        "resultStatus",
                        true
                    )

from the second screen and in the first screen which is ScreenA add

val noResultData =
                backStackEntry.savedStateHandle.getLiveData<Boolean>("resultStatus")
                    .observeAsState(false)

inside the composable(route){ backStackEntry -> } .This might fix your issue.

Once it is used you can remove it like this

backStackEntry.savedStateHandle.remove<Boolean>("resultStatus")
Callboard answered 3/8, 2022 at 12:23 Comment(2)
This wont work in my case as previousBackStackEntry would be of Screen B and Not Screen A.Congregational
You can first do the popBackStack and then set the result the currentBackStackEntry as this will be your target Screen A.Shadowy
E
4

I do this here https://github.com/google/horologist/blob/5884bfd051e5692dae874003ded80c4c39375c93/media-ui/src/main/java/com/google/android/horologist/media/ui/navigation/MediaNavController.kt#L64

It's effectively

    public fun NavController.navigateToXWithArgument() {
        navigate("player?page=0") {
            popUpTo("player?page={page}") {
                inclusive = true
                saveState = false
            }
        }
    }

I think it's important that the route in popUpTo does not have any real values, so it's exactly what you passed in when you defined the route.

In my case to read it, I use a LaunchedEffect to react when it gets set and remove it, but you may want something different.

    val pageParam = backStack.arguments?.getInt(page, -1) ?: -1
    backStack.arguments?.remove(page)

LaunchedEffect(pageParam) {
        if (pageParam != null) {
                // do something
        }
    }
Erbium answered 4/8, 2022 at 7:27 Comment(6)
Wont this be a forward navigation, killing all back stack?Congregational
From my testing it seems to work, navigating to my top level page, by using popUpTo. I thought you wanted to pop up to A, removing B1..BN from the backstack. Maybe I'm misunderstanding what you are after.Erbium
This might work but you are starting the same screen again clearing the back stack, the last retain state will be lostCongregational
I'll check this again, I was able to retain state in my testing.Erbium
Yep, I think you are right. Seems like a get a new instance of the ViewModel, in my case the results are the same, so not an issue.Erbium
This is frustrating. So basically the current workaround is to communicate the changed state to the ViewModel via some other Flow (e.g. in a Repository) or similar?Shadowy
S
1

I'm trialing a workaround for this scenario using the saveStateHandle to communicate the "return result".

  1. Pop backstack to target (static) route

  2. Get the current backstack entry (this is now the target) and modify it's savedStateHandle (e.g. add the "return argument").

  3. In the target composable setup a stateFlow for the savedStateHandle to collect the return argument.


Example:

navHostController.popBackStack("ScreenA")
navHostController.currentBackStackEntry?.savedStateHandle?.set(RETURN_RESULT_KEY, "SUCCESS") 

Then on the composable of ScreenA use the NavBackStackEntry from the NavGraphBuilder to retrieve the savedStateHandle

val popReturnResult by backStackEntry.savedStateHandle.getStateFlow(RETURN_RESULT_KEY, "EMPTY").collectAsState() 

In my case I'm passing it to the viewModel to make sense of it.

LaunchedEffect(popReturnResult) {
    if (popReturnResult != "EMPTY") {
        viewModel.handlePopReturnResult(popReturnResult)
        backStackEntry.savedStateHandle.remove<String>(RETURN_RESULT_KEY)
    }
}

There the viewModel will then receive a "SUCCESS" in handlePopReturnResult if it came from the right popBackStack call.

Either way I'll only be using this to communicate THAT there are results. The actual result state will be living in a global cache / repository somewhere else.

Shadowy answered 16/3, 2023 at 1:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.