Elforama
Elforama

Reputation: 652

Jetpack Compose navigate for result

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.

Upvotes: 47

Views: 21816

Answers (10)

Mahdi Zareei
Mahdi Zareei

Reputation: 2066

in second screen you can send result like this:

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

in parent screen you can listen to the result as effect like this:

navController.currentBackStackEntry?.savedStateHandle?.getStateFlow("result_key", false)?.collectAsEffect {
        it.takeIf {
            it
        }?.also {
            //do something
        }
}

you should write this extension fun to collect as effect:

@Composable
fun <T> Flow<T>.collectAsEffect(
    context: CoroutineContext = EmptyCoroutineContext,
    block: (T) -> Unit
) {
    LaunchedEffect(key1 = Unit) {
        onEach(block).flowOn(context).launchIn(this)
    }
}

Upvotes: 1

DataGraham
DataGraham

Reputation: 1645

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

Upvotes: 0

pfchen
pfchen

Reputation: 97

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

Upvotes: 6

SaharVX9
SaharVX9

Reputation: 910

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

Upvotes: 0

Hezy Ziv
Hezy Ziv

Reputation: 5568

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"
}}

Upvotes: 0

user2379236
user2379236

Reputation: 11

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")
}

Upvotes: 1

abbasalim
abbasalim

Reputation: 3232

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)

Upvotes: 0

Evgenii Doikov
Evgenii Doikov

Reputation: 431

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
}

Upvotes: 14

CTD
CTD

Reputation: 346

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

Upvotes: -1

nglauber
nglauber

Reputation: 24044

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
}

Upvotes: 66

Related Questions