33

I'm using the Jetpack Navigation library with the Compose version. I'm setting up navigation like it's shown here

I want to be able to navigate from screen A to screen B. Once B does something and pops off the back stack, it will then return a result that screen A can access.

I found a way to do this using Activities here but I want to avoid creating any extra activities and do this in compose.

Elforama
  • 512
  • 1
  • 4
  • 16
  • 3
    The ability to [return a result](https://developer.android.com/guide/navigation/navigation-programmatic#returning_a_result) also works in Navigation Compose; it is a core part of Navigation. – ianhanniballake Mar 28 '21 at 00:44

9 Answers9

49

From the Composable that you want to return data, you can do the following:

navController.previousBackStackEntry
    ?.savedStateHandle
    ?.set("your_key", "your_value")
navController.popBackStack()

and then, from the source Composable, you can listen for changes using a LiveData.

val secondScreenResult = navController.currentBackStackEntry
    ?.savedStateHandle
    ?.getLiveData<String>("your_key")?.observeAsState()
...
secondScreenResult?.value?.let {
    // Read the result
}
nglauber
  • 18,674
  • 6
  • 70
  • 75
  • Did you actually try it out? Because for me the receiving value is always `null`. – Florian Walther Aug 05 '21 at 10:53
  • Your answer saved my time. Thanks – Polaris Nation Nov 12 '21 at 07:06
  • 4
    don't forget remove your key when you consumed. – CTD Dec 16 '21 at 08:52
  • 1
    Is there a way to do this using the SavedStateHandle that is in the ViewModel rather than listening in the composable? – Scott Cooper Dec 24 '21 at 16:00
  • @ScottCooper Yes, see my post below. – CTD Jan 18 '22 at 11:15
  • 4
    You have to add `implementation "androidx.compose.runtime:runtime-livedata:$compose_version"` to gradle for `observeAsState()` to work (replace `$compose_version` with the actual version). – lenooh Mar 15 '22 at 17:32
  • What if I am traversing through A>B>C and I want to return data from C to A, how do i Do that as previousBackStackEntry will give me entry for B not A – Cyph3rCod3r Aug 03 '22 at 10:13
  • 2
    if you need to return some data from C to A, you should return from C to B, then return from B to A. But if you pop B when you call C, then A would be the previous backstack of C, so it should work. – nglauber Aug 03 '22 at 11:46
9

If you need only once get value, you need remove value after usage:

val screenResultState = navController.currentBackStackEntry
    ?.savedStateHandle
    ?.getLiveData<String>("some_key")?.observeAsState()

screenResultState?.value?.let {
    ...
    // make something, for example `viewModel.onResult(it)`
    ...
    //removing used value
    navController.currentBackStackEntry
        ?.savedStateHandle
        ?.remove<String>("some_key")
}

I also extract it in function (for JetPack Compose)

@Composable
fun <T> NavController.GetOnceResult(keyResult: String, onResult: (T) -> Unit){
    val valueScreenResult =  currentBackStackEntry
        ?.savedStateHandle
        ?.getLiveData<T>(keyResult)?.observeAsState()

    valueScreenResult?.value?.let {
        onResult(it)
       
        currentBackStackEntry
            ?.savedStateHandle
            ?.remove<T>(keyResult)
    }
}

you can copy it to your project and use like this:

navController.GetOnceResult<String>("some_key"){
    ...
    // make something
}
Evgenii Doikov
  • 372
  • 3
  • 10
  • 1
    `?.remove` should be `?.remove` – Olaf Achthoven May 09 '22 at 14:23
  • Thanks, Olaf Achthoven, you are right. – Evgenii Doikov May 10 '22 at 10:03
  • there is a nuance: removing key from the state handle remove the live data subscription. So, it's better to set key to null: ``` @Composable fun NavController.GetOnceResult(key: String, onResult: (T) -> Unit) { val state = (currentBackStackEntry ?: return) .savedStateHandle .getLiveData(key).observeAsState() if (state.value == null) return state.value?.also { onResult(it) currentBackStackEntry?.savedStateHandle?.set(key, null) } } ``` – Oleksii Malovanyi May 25 '22 at 16:55
  • But you can't use it inside the viewmodel, cause it is a composable function. – EliodeBeirut Dec 09 '22 at 08:27
  • Good point by https://stackoverflow.com/users/691993/oleksii-malovanyi. I've added an answer with more detail, because I found that rather than just a nuisance, I was actually getting duplicate observations. – DataGraham Mar 22 '23 at 20:32
  • I don't think it's a good idea to use NavController inside compose screen, you should pass only the data to your compose screen but not NavController – user924 Apr 27 '23 at 17:01
3

The top answer is good enough for the most situations, but I find it isn't easy to work with a ViewModel if you want to do something in a method of ViewModel. Instead of using a LiveData or a Flow to observe the result from the called screen, I use the callback to solve this problem.

I hope my answer can help some people.

import androidx.navigation.NavController

/**
 * The navigation result callback between two call screens.
 */
typealias NavResultCallback<T> = (T) -> Unit

// A SavedStateHandle key is used to set/get NavResultCallback<T>
private const val NavResultCallbackKey = "NavResultCallbackKey"

/**
 * Set the navigation result callback on calling screen.
 *
 * @param callback The navigation result callback.
 */
fun <T> NavController.setNavResultCallback(callback: NavResultCallback<T>) {
    currentBackStackEntry?.savedStateHandle?.set(NavResultCallbackKey, callback)
}

/**
 *  Get the navigation result callback on called screen.
 *
 * @return The navigation result callback if the previous backstack entry exists
 */
fun <T> NavController.getNavResultCallback(): NavResultCallback<T>? {
    return previousBackStackEntry?.savedStateHandle?.remove(NavResultCallbackKey)
}

/**
 *  Attempts to pop the controller's back stack and returns the result.
 *
 * @param result the navigation result
 */
fun <T> NavController.popBackStackWithResult(result: T) {
    getNavResultCallback<T>()?.invoke(result)
    popBackStack()
}

/**
 * Navigate to a route in the current NavGraph. If an invalid route is given, an
 * [IllegalArgumentException] will be thrown.
 *
 * @param route route for the destination
 * @param navResultCallback the navigation result callback
 * @param navOptions special options for this navigation operation
 * @param navigatorExtras extras to pass to the [Navigator]
 *
 * @throws IllegalArgumentException if the given route is invalid
 */
fun <T> NavController.navigateForResult(
    route: String,
    navResultCallback: NavResultCallback<T>,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null
) {
    setNavResultCallback(navResultCallback)
    navigate(route, navOptions, navigatorExtras)
}

/**
 * Navigate to a route in the current NavGraph. If an invalid route is given, an
 * [IllegalArgumentException] will be thrown.
 *
 * @param route route for the destination
 * @param navResultCallback the navigation result callback
 * @param builder DSL for constructing a new [NavOptions]
 *
 * @throws IllegalArgumentException if the given route is invalid
 */
fun <T> NavController.navigateForResult(
    route: String,
    navResultCallback: NavResultCallback<T>,
    builder: NavOptionsBuilder.() -> Unit
) {
    setNavResultCallback(navResultCallback)
    navigate(route, builder)
}

A example of usage:


fun NavGraphBuilder.addExampleGraph(navController: NavController) {
    composable(FirstScreenRoute) {
        FirstScreen(
            openSecondScreen = { navResultCallback ->
                navController.navigateForResult(SecondScreenRoute, navResultCallback = navResultCallback)
            },
            ... // other parameters
        )
    }

    composable(SecondScreenRoute) {
        SecondScreen(
            onConfirm = { result: T ->  // Replace T with your return type
                navController.popBackStackWithResult(result)
            },
            onCancel = navController::navigateUp,
            ... // other parameters
        )
    }
}
pfchen
  • 49
  • 2
  • This crashes if your callback if e.g. a viewmodel method. The way to reproduce: go to ResultScreen, then switch to another app and then switch back You're getting `NotSerializableException` – Simon Apr 04 '23 at 16:52
0

for jetpack compose you must use Flow with collectAsState for get result:

navController.currentBackStackEntry
?.savedStateHandle?.getStateFlow<Boolean?>("refresh", false)
?.collectAsState()?.value?.let {
if (it)screenVM.refresh() }

also you can remove Entry with add this after screenVM.refresh():

 navController.currentBackStackEntry
                ?.savedStateHandle ?.set("refresh", false)
abbasalim
  • 3,118
  • 2
  • 23
  • 45
0

You can get the result without a LiveData or a Flow, you can use savedStateHandle.remove method. I think this is the easier way:

val secondResult = appNavController.currentBackStackEntry?.savedStateHandle?.remove<Data?>("data")
secondResult?.let { data ->
    Log.d(TAG, "Data result: $data")
}
JustSightseeing
  • 1,460
  • 3
  • 17
  • 37
0

add dependency

implementation "androidx.compose.runtime:runtime-livedata:$compose_version"

On the Sender screen set a key value pair to send back to the caller screen, I use a Boolean with key name of "key" value true

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

navigate up

navController.navigateUp()

The receiver screen (caller) listens to the results and then remove it:

 val result =  navController.currentBackStackEntry?.savedStateHandle
    ?.getLiveData<Boolean>("key")?.observeAsState()
result?.value?.let {
    navController.currentBackStackEntry?.savedStateHandle
        ?.remove<Boolean>("key")
}

First Screen

@Composable fun FirstScreen(navController: NavController){

val result =  navController.currentBackStackEntry?.savedStateHandle
    ?.getLiveData<Boolean>("key")?.observeAsState()
result?.value?.let {
    navController.currentBackStackEntry?.savedStateHandle
        ?.remove<Boolean>("key")
}

Button(onClick = {
    navController.navigateUp("secondScreen")
}) {
    "Open second screen"
}}
Hezy Ziv
  • 3,652
  • 2
  • 14
  • 8
0

If you want to return from PageC to PageA and pop pageB without return to it i found solution :

wait for result from screenA

composable("ScreenA") {
  val savedStateHandle = navController.currentBackStackEntry?.savedStateHandle
  val result by savedStateHandle.getStateFlow("key").collectAsState()
  ScreenA(result)
}

Return value from screen3

  navController.apply {
            backQueue.firstOrNull { it.destination.route == route }?.savedStateHandle?.set("key",true) //
            popBackStack(route, inclusive)
        }

after you get your the desired answer delete it from page3 cus you save it on saveStateHandle of the page

savedStateHandle.remove<Boolean>("key")
alien123X
  • 719
  • 1
  • 7
  • 12
0

Referencing nglauber's answer, I found I was getting repeated observations of the same result, until I switched from calling SavedStateHandle.remove() to setting the value of the LiveData to null instead.

@Composable
fun <T> NavBackStackEntry.GetOnceResult(resultKey: String, onResult: (T) -> Unit) {
    val resultLiveData = savedStateHandle.getLiveData<T>(resultKey)
    resultLiveData.observeAsState().value?.let {
        resultLiveData.value = null
        onResult(it)
    }
}

See, SavedStateHandle.getLiveData() actually returns a MutableLiveData, rather than just a generic LiveData. I was quite surprised as first, until I realized that this must be intentional, to let you modify the saved state via the MutableLiveData (which it does in fact do, as opposed to simply modifying the LiveData itself).

I got this idea when I saw the documentation for SavedStateHandle.remove():

Removes a value associated with the given key. If there is a LiveData and/or StateFlow associated with the given key, they will be removed as well. All changes to androidx.lifecycle.LiveDatas or StateFlows previously returned by SavedStateHandle.getLiveData or getStateFlow won't be reflected in the saved state. Also that LiveData or StateFlow won't receive any updates about new values associated by the given key.

I added some logging to confirm that while normally, the call to getLiveData() on each recomposition returns the same LiveData instance again, calling SavedStateHandle.remove() causes it to subsequently return a different LiveData (which gives you the old value, causing the duplicate observation).

DataGraham
  • 1,625
  • 1
  • 16
  • 20
-1
val navController = rememberNavController()
composable("A") {
    val viewmodel: AViewModel = hiltViewModel()
    AScreen()
}
composable("B") {
    val viewmodel: BViewModel = hiltViewModel()
    val previousViewmodel: AViewModel? = navController
        .previousBackStackEntry?.let {
            hiltViewModel(it)
        }
    BScreen(
       back = { navController.navigateUp() },
       backWhitResult = { arg ->
           previousViewmodel?.something(arg)
       }
    )
}
CTD
  • 306
  • 3
  • 8