Hey everyone, how do I send a one-time event from ...
# compose
t
Hey everyone, how do I send a one-time event from the viewmodel to the composable? I want to navigate after a long operation
šŸ‘ 1
t
SharedFlow?
t
Hi @Tgo1014, but how do I use it in the composable? Should I use a SideEffect? If so, which kind?
In the docs https://developer.android.com/jetpack/compose/side-effects#sideeffect-publish they show a similar example, with a snackbar, using LaunchedEffect. However, I don’t really need the navigation logic to be in a suspend block, i don’t think. So I’m not sure if it’s the right kind of side effect that I want
(also, I’ll probably be using a StateFlow)
t
You can just collect the flow as state and react accordingly in the composable
t
I really don’t think it’s a good idea to have navigation logic (or any kind of logic that isn’t related to what’s going to be drawn) directly in the composable
šŸ’Æ 1
I’ll just use LaunchedEffect for now
n
Copy code
private val _errorEvent = MutableSharedFlow<ErrorEntity.Error?>(replay = 0)
This avoid the event to be emitted twice šŸ˜‰
t
Thanks, that is what we used to do with fragments
But still I’m pretty sure it’s bad to have the navigation logic directly in the composable
ComposablesĀ should be side-effect free
AĀ *side-effect*Ā is a change to the state of the app that happens outside the scope of a composable function
n
My navigation logic is not in my composable šŸ™‚ I keep it in the NavGraph. I just pass lambdas/callbacks to my composables. šŸ˜‰
t
AnĀ *effect*Ā is a composable function that doesn't emit UI and causes side effects to run when a composition completes
Hm
Anyway thanks, that solution should work in any case šŸ™‚
šŸ‘ 1
m
Hi šŸ‘‹ What are you using for navigation? Are you using the Navigation component? What about passing the
navController
as a dependency to the ViewModel so that the VM can handle the event instead of sending it to the composable?
😧 1
t
Hi @Manuel Vivo! If you say that sending the navController to the viewModel isn’t bad practice, I’ll do it šŸ˜„
m
Well… you can make the ViewModel be the source of truth for the
navController
instead of a composable. There’s nothing wrong with that
We’re coming up with guidance for events in Android. But it’s taking longer than expected. So far, the rules of thumb we have are: • Let the ViewModel handle the event whenever it’s possible, and • If the event is critical for the screen, model it as state
šŸ‘ 4
With Compose, #1 is possible because the Navigation component hoists its state via
navController
. This is something that’s not possible with the View system where the navigation state is a black box inside the
NavHost
itself
I see some panic reactions in my comment above šŸ˜„ I’d love to know your thoughts
As I was saying, all of this is WIP and we’re trying to come up with the best guidance
n
I added the reaction @Manuel Vivo šŸ˜… As I mentioned above, I’m keeping the navigation logic in my ā€œNavigationRouterā€ and just passing the lambdas to my screens.
Copy code
NavHost(...) {
        composable("ScreenA") { 
            ScreenA(
                onBack = { navController.navigateUp() },
                onNext = { navController.navigate("ScreenB") }
            )
        }
        composable("ScreenB") { 
            ScreenB(
                onBack = { navController.navigateUp() },
                onNext = { navController.navigate("ScreenC") }
            )
        }
        ...
}
šŸ‘ 1
a
m
Yeah, that’s a totally valid use case as well. In fact, that’s the way to go. But you could also have a state holder class in this composable that contains the
navController
and handles all interactions with it
⭐ 1
c
Guidance for this can't come soon enough. Lol. Analysis paralysis is real! I'm starting a brand new app and just want to know what to do in the typical case.
m
If we’re talking about Compose specifically, I think it’s safe to say that the state holder (that could be a ViewModel) should process the events and make state changes to reflect that on the UI. i.e. do you need to show a snackbar? The state holder can take be the SoT for the
snackbarHostState
, and call
showSnackbar
when needed. The same pattern applies for
navController.navigate
calls.
šŸ‘ 2
c
You'd use state for a navigation event?
m
Depending on your use case, the solution can be more contrived. For example, if you only certain type of events to be processed when
LocalLifecycleOwner
is at least
STARTED
You’d use state for a navigation event?
Nope. You’d pass the
navController
to the state holder (that would then become the SoT for that state) and call
navigate
directly from the state holder
šŸ‘ 1
c
Do you think any of the compose sample apps do a good job at showing how navigation should be done in a pure compose app? I'm still hung up on how an app with a login screen would work + de authenciation would kick them out of the app.
āž• 1
m
That’s at least what I got from internal conversations with @Adam Powell šŸ™‚ I hope he agrees with this šŸ˜…
c
Passing navController to VM? I wonder if that keeps the AAC VM testable. Makes sense at a high level to me. But boy oh boy. Do we need more docs around this IMO.
m
Do you think any of the compose sample apps do a good job at showing how navigation should be done in a pure compose app?
You have to bear with us šŸ™‚ this is also very new to us and we’re experimenting. In fact, I think there are only a couple of samples that use Compose Navigation. So I agree we need to cover more ground here
šŸ¤— 1
šŸ‘ 1
I wonder if that keeps the AAC VM testable
Most of the state involved here is hoisted, so yes, this is all very testable šŸ™‚
c
My question was mostly towards if the VM is testable rather than composable.
"bear with us" sounds good to me. I might go back to fragments with composables for the time being then. I at least know how to make it work with logging in and deauth lol
m
My question was mostly towards if the VM is testable rather than composable.
Yeah, the VM is also testable because it takes the other hoisted states (e.g.
navController
and
snackbarHostState
) as dependencies so you can assert interactions with them and even fake them if needed
šŸ’Æ 1
t
We’re also using KMM, but we’re not sharing the VM (yet). If we share the VM at some point, we might need to find another solution (because there might not be a way to supply the VM with a ā€œnavControllerā€ in the iOS side). In this use case, having a ā€œdumbā€ VM sounds best. Of course this is out of scope, but still something to consider šŸ™‚
m
Ideally in iOS, you should also be able to hoist the navigation state. If that’s the case, you could hide the differences between the Android and iOS implementations under an interface, and let your state holder take that instead
šŸ‘Œ 2
I’m purposely using state holder instead of VMs because the latter is an implementation detail IMO
šŸ‘ 1
t
Sounds perfect, let’s hope the fruit doesn’t disappoint us later šŸ™‚
n
Not to derail the discussion, but I use a Channel to send one-time events from a viewModel to a Fragment. Does that pattern still hold in the compose world, and if so how would one collect this type of flow from a composable?
a
you can do it but it creates edge cases to handle in handoff from one fragment instance to another and other point of responsibility management
hoisting your navigation state to a lifetime at or "above" your viewmodel is an option, but it's not strictly necessary
n
Oh I'm thinking in a compose only app. I guess the question would be how to collect a flow from a composable, just not one that's related to State.
t
@Tiago Nunes I use
MutableSharedFlow
with
replay=0
and
extraBufferCapacity = 1
. Here is an example of it.
m
Hi everyone! I had some time to write actual code with the thoughts I shared in this thread. Here’s a version of events being 100% handled by the state holder that also survive activity and process recreation. To complicate the example even more, certain events (in this case snackbar messages) are processed only when the lifecycle is at least
STARTED
https://github.com/android/compose-samples/pull/608/files I’d love to know your thoughts! Disclaimer: this is an experiment and shouldn’t be taken as guidance by any means šŸ™ƒ
šŸ™ 3
šŸ’Ŗ 5