svenjacobs
05/21/2024, 1:48 PMdropUnlessResumed
API for navigation from Lifecycle 2.8.0
and how to deal with navigation callbacks that have arguments.svenjacobs
05/21/2024, 1:48 PM2.8.0-beta01
because LocalLifecycleOwner
was moved to a new package in Lifecycle 2.8.0
.
Anyway, the release notes even show a simple example.
onClick: () -> Unit = dropUnlessResumed { navController.navigate(NEW_SCREEN) }
Also, according to the official documentation we should expose navigation events from composables (callbacks) and not pass in the NavController
into composables, which is a good recommendation.
So what I usually do is that I bubble up all those navigation events to the place where the route is defined, where NavController
is instantiated and where NavHost
is placed. Then inside of all those navigation callbacks I call navController.navigate(ROUTE)
or navController.popBackStack()
, whatever is required. But of course there are routes that have arguments. Let’s imagine a simple onItemClick: (itemId: String) -> Unit
which should open a detail screen of that specific item. However dropUnlessResumed
only accepts lambdas without arguments (() -> Unit
) so how am I supposed to use this “wrapper” for callbacks with arguments?
Do I get this right that I have to use dropUnlessResumed
inside each composable where the original click event originates and before I enrich it with additional arguments? Something like
@Composable
fun Item(
itemId: String,
title: String,
onItemClick: (itemId: String) -> Unit,
) {
Column {
Text(title)
Button(
onClick = dropUnlessResumed {
onItemClick(itemId)
}
) {
Text("Show item")
}
}
}
I don’t know but it kind of feels wrong. I should not have to add this function somewhere deep in my composable hierarchy, which is mostly only required for navigation. I would rather like to have this safeguard at the place where navigation is defined and performed.
What do you think? Do you think this is okay? Did you find another solution?Stylianos Gakis
05/21/2024, 1:59 PMonFoo: (NavBackStackEntry, Param) -> Unit,
so that I can hoist the "do not do this when not resumed" part up to the highest point, and no screen itself needs to know about this detail. Since from the NavBackStackEntry
you can grab its lifecycle to conditionally do nothing.
On the other hand, there have been times where I do in fact want something to be done even we are not in resumed state (bottom sheet is dismissing for example) and in those cases I suddenly had to know in my NavGraphBuilder extension function that this lambda had to not guard against this lifecycle check.
Perhaps the suggestion with this is to in fact do this check inside the screen itself. Or at least at the top-level of your screen if anything, so you get full control there.
Dropping this check all the way down to some small Item
composable would be very susceptible to making mistakes for sure. Idk how your screens typically look like, but what I do is something like this, where the destination-level composable takes in a VM and then there is a more generic screen level one. I could imagine adding the dropUnlessResumed
calls at that destination
composable for example, so that it's still high enough that small composables don't need to know about it, while also grabbing the correct lifecycle and allowing some lambdas not to do this if you know you don't want them to be drop unless resumed?Stylianos Gakis
05/21/2024, 2:00 PMPablichjenkov
05/21/2024, 2:09 PMdrop
kinda keeps me away from itsvenjacobs
05/21/2024, 2:09 PMDropping this check all the way down to some smallYou’re right butcomposable would be very susceptible to making mistakes for sure.Item
dropUnlessResumed
is a @Composable
function and therefor requires a compose context. This is because it accesses LocalLifecycleOwner
to check its current state and drop the function if necessary.
So once I have my regular, augmented callback onItemClick: (itemId: String) -> Unit
it’s not possible to call dropUnlessResumed
inside of it some layers above.svenjacobs
05/21/2024, 2:10 PMHaven’t used it but the wordWhy? It actually makes sense and solves a common problem in Compose navigation.kinda keeps me away from itdrop
Stylianos Gakis
05/21/2024, 2:10 PMdrop
is just a word, and what this API does is in fact a common use case for some scenarios, especially around navigation as we're discussing here.
Not all "drop" are problematic. It depends on the context.Pablichjenkov
05/21/2024, 2:11 PMStylianos Gakis
05/21/2024, 2:11 PMStylianos Gakis
05/21/2024, 2:11 PMsvenjacobs
05/21/2024, 2:12 PMpopBackStack()
multiple times for example when someone clicked too fast on the back/close button. It may cause undefined behaviour in your app.Pablichjenkov
05/21/2024, 2:14 PMStylianos Gakis
05/21/2024, 2:24 PMcomposable
lambda. And indeed I do not see a nice way to make this work along with parameters, which you are very often going to have 🤷svenjacobs
05/21/2024, 2:25 PMdropUnlessResumed
that goes beyond some simple examples of navigation events without any arguments. Because usually we do have arguments for our navigation events.Stylianos Gakis
05/21/2024, 2:27 PMdropUnlessResumed { doThing(paramYouHaveAccessToInThisContext) }
there.
Again, maybe this kinda is the "right" way to do it, since it's only at the lower lever that you really know if you want to drop follow-up invocations or not, so maybe that's what this API is supposed to be used likesvenjacobs
05/21/2024, 2:30 PMItem
itself should not have to know that its callback should or must be dropped at a certain lifecycle state. I think this makes the Item
too aware of the outside conditions (e.g. we’re using Navigation Compose) and probably breaks some architecture and best practices recommendations 😉Stylianos Gakis
05/21/2024, 2:30 PMdropUnlessResumed
Stylianos Gakis
05/21/2024, 2:36 PMI think anWell technically it doesn't know anything about navigation despite this, this is lifecycle related, not navigation related. The lifecycle just happens to be driven by navigation in this case. And often it really is the case that only the real call-site (and not the ones just delegating it down) know if it wants to respect the lifecycle or not. When done on the nav-graph level, I did remember now that I've also had problematic scenarios where a navigation lambda was hit by a button, so I protected it from the lifecycle like that. Then later, some state was also driving this navigation, basically if some state was set, we were navigating there and resetting the state. In that case we were sometimes silently dropping this nav event, despite clearing the state. Again, I don't have the answers here, I am also quite curious myself how can one protect themselves as best as possible from misusing this API and nav in general with it.itself should not have to know that it’s callback should or must be dropped at a certain lifecycle state. I think this makes theItem
too aware of the outside conditions (e.g. we’re using Navigation Compose) and probably breaks some architecture and best practices recommendations 😉Item
svenjacobs
05/21/2024, 2:48 PMsvenjacobs
05/21/2024, 6:56 PMdropUnlessResumed
which is not a composable function.
fun dropUnlessResumed(
lifecycleOwner: LifecycleOwner,
block: () -> Unit,
) {
if (lifecycleOwner.lifecycle.currentState.isAtLeast(State.RESUMED)) {
block()
}
}
So I can use it like this
NavHost(...) {
composable("main") { navBackStackEntry ->
MainScreen(
onItemClick = { itemId ->
dropUnlessResumed(navBackStackEntry) {
navController.navigate("itemDetail?id=$itemId")
}
}
)
}
}
Stylianos Gakis
05/21/2024, 7:05 PMsvenjacobs
05/21/2024, 7:13 PMStylianos Gakis
05/21/2024, 7:16 PMyoussef hachicha
05/22/2024, 7:16 AMsvenjacobs
05/22/2024, 7:19 AMNavBackStackEntry
provided by Navigation is itself a LifecycleOwner
and the state is changed when a navigation is taking place. dropUnlessResumed
takes the lifecycle from LocalLifecycleOwner
and thus drops the second click event when the state is not RESUMED
.Stylianos Gakis
05/22/2024, 7:21 AMStylianos Gakis
05/22/2024, 7:22 AMyoussef hachicha
05/22/2024, 7:26 AMsvenjacobs
05/22/2024, 7:27 AMNavBackStackEntry
is bound to the lifecycle of the navigation, not of the app or the surrounding composable.Stylianos Gakis
05/22/2024, 7:30 AMStylianos Gakis
06/14/2024, 2:01 PMonItemClick
for example does in fact want the even dropped if the state is not resumed?
I was doing some refactoring on some screen today and I figured that some lambdas did and some did not want to drop the event, and I had very low confidence that I wasn't breaking anything in my refactor so I had to go inside those small composables as we said and look when the lambda is actually invoked to know if I want or don't want to respect the lifecycle. Quite brittle and I am sure I can easily introduce bugs here.
The more I think about this, maybe we do want that little composable to call dropUnlessResumed
itself if it knows it's doing a navigation event. Which I suppose is only "enforceable" through naming conventions on the lambdas that do navigation.svenjacobs
06/14/2024, 2:04 PMStylianos Gakis
06/14/2024, 2:14 PMsvenjacobs
06/14/2024, 2:47 PM