Michal Klimczak
10/25/2021, 8:51 AM/**
* Observes the callback result from previous NavController backStackEntry and makes sure,
* that it's cleared so the LiveData doesn't trigger again (by default it triggers with the old value
* for every resubscription)
*
* Ref: <https://developer.android.com/guide/navigation/navigation-programmatic#returning_a_result>
*/
fun <T> NavController.navigationCallbackFlow(argKey: String) = this
.currentBackStackEntry!!
.savedStateHandle
.getLiveData<T>(argKey)
.asFlow()
.map { this.currentBackStackEntry!!.savedStateHandle.get<T>(argKey) }
.onEach { this.currentBackStackEntry!!.savedStateHandle.remove<T>(argKey) }
.filterNotNull()
Tin Tran
10/25/2021, 8:54 AMMichal Klimczak
10/25/2021, 8:57 AMTin Tran
10/25/2021, 8:59 AMTin Tran
10/25/2021, 9:01 AMnavigation(startDestination = "nestedB", route = "b") {
composable(route = "nestedB") {
val backStackEntry =
remember { navController.getBackStackEntry("b") }
ComposableB(
navController = navController,
hiltViewModel(backStackEntry)
)
}
composable(route = "nestedC") {
val backStackEntry =
remember { navController.getBackStackEntry("b") }
ComposableC(
navController = navController,
hiltViewModel(backStackEntry)
)
}
}
Michal Klimczak
10/25/2021, 11:47 AMstartDestination
and route
but with a different path e.g. product/{productId}
and productGraph/{productId}
, right?Ian Lake
10/25/2021, 1:36 PMMichal Klimczak
10/25/2021, 1:48 PMpreviousBackStackEntry.savedStateHandle.set(...)
) and invoked popBackStack.
• ComposableA wants to show some snackbar to show that there's been success
It works fine, but then if you navigate from ComposableA to anything else and go back, the livedata will return again showing that snackbar.
Of course you can navigate to ComposableB again and perform this action again. Or just pop the back stack by navigating back. And the snackbar should appear or not appear accordingly.Ian Lake
10/25/2021, 5:46 PM// Never use navController.currentBackStackEntry inside of your
// composable screen as that value changes immediately when you
// call navigate, despite your screen still being recomposed during
// the exit animations. Always use the NavBackStackEntry passed to you.
val savedStateHandle = it.savedStateHandle
val deletedItemId = savedStateHandle.getLiveData<String>(RESULT_KEY)
.observeAsState(null)
if (deletedItemId != null) {
// I assume you already have a snackbarHostState declared in
// your composable or as part of a ScaffoldState: use that here
LaunchedEffect(snackbarHostState, deletedItemId) {
try {
val result = snackbarHostState.showSnackbar(
message = "Item deleted"
actionLabel = "Undo"
)
if (result == SnackbarResult.ActionPerformed) {
// Trigger your undo
}
} finally {
// Use a finally block to *always* run this code, even if your
// snackbar hasn't been removed before the user navigated to a
// new screen: this is what ensures the Snackbar only is shown once
savedStateHandle.remove(RESULT_KEY)
}
}
}
Ian Lake
10/25/2021, 5:49 PMSaveStateHandle.remove
as part of composition (that's a side-effect and composition should always be side effect free)Michal Klimczak
10/26/2021, 6:48 AMSavedStateHandle.remove
later, the LiveData will still hold on to the last value it emitted. And will redeliver it when subscribed again. I guess that val deletedItemId = savedStateHandle.getLiveData<String>(RESULT_KEY).observeAsState(null)
will prevent it from being delivered again, because it's conflated, but that doesn't completely solve the problem. Let me explain.
Following your deletedItemId
example, after I delete item 123
in the ComposableB started for result (e.g. dialog) I want both these scenarios to work fine:
1. I open ComposableB again, and return from it with back button - the snackbar should not appear.
2. I open ComposableB again and delete item 123
again (e.g. because the previous deleting was undone) - the snackbar should appear again.
In the piece of code above, the second scenario won't work, right?Tin Tran
10/26/2021, 6:54 AMbut it’s a bit problematic when it comes to passing arguments... I mean if I wanted to attach a subgraph for e.g. some ProductDetails with some productId argument, then I would have to basically copy the same root pattern forYeah. For me it should be like thisandstartDestination
but with a different path e.g.route
andproduct/{productId}
, right?productGraph/{productId}
product/{productId}
product/{productId}/subGraph1
product/{productId}/subGraph2
Ian Lake
10/26/2021, 1:07 PMremove
clears the LiveDataMichal Klimczak
10/26/2021, 1:38 PMMichal Klimczak
10/26/2021, 2:56 PM// Never use navController.currentBackStackEntry inside of your
// composable screen as that value changes immediately when you
// call navigate, despite your screen still being recomposed during
// the exit animations. Always use the NavBackStackEntry passed to you.
That means that we can't use the savedStateHandle passed to the viewmodel and handle this logic there - we need to grab it directly inside the composable and there's no way around it, right? In the current project I have koin, but the same will be true for hilt I guess.Desmond Teo
10/26/2021, 3:14 PMIan Lake
10/26/2021, 3:34 PMSavedStateHandle
associated with an individual VIewModel (remember: you can create many, many ViewModels for the same destinatinon and each has its own SavedStateHandle
) is not the same instance as `navBackStackEntry.savedStateHandle`; they aren't interchangeable. If you're using navBackStackEntry.savedStateHandle
, then your 'current' NavBackStackEntry is the one passed to your composable
destinationDesmond Teo
10/26/2021, 3:50 PMcomposable("routeA") { navBackStackEntry ->
val savedStateHandle = navBackStackEntry.savedStateHandle
val deletedItemId = savedStateHandle.getLiveData<String>(RESULT_KEY)
.observeAsState(null)
LaunchedEffect(snackbarHostState, deletedItemId) {
try {
// do something
} finally {
savedStateHandle.remove(RESULT_KEY)
}
}
}
bottomSheet("routeB") {
// in some click handler
navController.previousBackStackEntry?.savedStateHandle?.set(RESULT_KEY, "deleted_id")
}
I’m using navBackStackEntry.savedStateHandle
like this, but i noticed in routeA, the livedata will only receive the first result. if i navigate to routeB bottom sheet again to set the result, the livedata in routeA composable will not receive further updates since savedStateHandle.remove(RESULT_KEY)
will detach the livedata.Ian Lake
10/26/2021, 3:56 PMobserveAsState()
), savedStateHandle.getLiveData<String>(KEY).value = null
will do the same thing as remove
, but without the detach of the LiveDataDesmond Teo
10/26/2021, 4:00 PMIan Lake
10/26/2021, 4:02 PMobserveAsState(null)
would need to be fixed, wouldn't it 🙂 You can use whatever sentinel value you want instead of nullMichal Klimczak
10/26/2021, 4:43 PMIan Lake
10/26/2021, 4:45 PMthe savedStateHandle passed through the navigation lambda will be recreatedNo, you always get the same
savedStateHandle
back for the same NavBackStackEntry
Michal Klimczak
10/26/2021, 4:49 PMIan Lake
10/26/2021, 4:49 PMgetLiveData()
that returns a brand new LiveData
instance after you remove
the previous instance - holding onto that previous instance after a remove
call would be the same type of issue that Desmond pointed outMichal Klimczak
10/26/2021, 4:50 PMMichal Klimczak
10/26/2021, 4:52 PMIan Lake
10/26/2021, 4:53 PMcurrentBackStackEntry
is when it comes to animations - when you navigate from A to B, you don't go from one composition of just A to one composition of just B, but there's a time during the animation that both A and B are recomposing. However, during that time, currentBackStackEntry
is always the final destination (B) even when you are accessing navController.currentBackStackEntry
from the recomposing screen AIan Lake
10/26/2021, 4:54 PMnavController.currentBackStackEntry
in screen A, you won't actually get the screen A NavBackStackEntry
- that's why you are specifically passed A's NavBackStackEntry
- that's what you need to be using anytime you are in screen AMichal Klimczak
10/26/2021, 5:01 PMIan Lake
10/26/2021, 5:51 PMSavedStateHandle
- if you look at navBackStackEntry.savedStateHandle
, all it does is create its own ViewModel and access its SavedStateHandle
: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigati[…]/src/main/java/androidx/navigation/NavBackStackEntry.kt;l=118Michal Klimczak
10/28/2021, 6:42 AMMichal Klimczak
11/17/2021, 7:55 PM