Reputation: 652
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
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
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
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
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
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
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
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
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
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
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