Question about handling events in Compose and best...
# compose
r
Question about handling events in Compose and best practices. Reading this article it's recommended not to use channel but rather a state to handle events in Compose, mainly because
Channel
doesn't guarantee delivery in some cases. But is it a bad practice to separate the event class from the ui state class? I've attached an example. Mainly because I don't think a ui event should trigger the re-composition of ui state rendered by compose, the
OneShotEventEffect
would just re-compose itself. Or are we supposed to have the entire state of the ui in a single class?
a
One of the tricky parts here when looking at a specific implementation is how it comes down to naming and the associated expectations. When you have a single
MutableStateFlow<MyUiEvent>
, it reads like you are prolonging the handling of an event and keeping it in-flight for longer to be handled later. When you call
triggerEvent
, that sort of reads as “tell the UI to do this thing” (both of which are antipatterns as described in the article) Telling the UI to do something is not directly possible. If the UI isn’t available, then multiple calls to
triggerEvent
will clear out the previous “event” that was sent, without it being handled. Maybe that’s what you want, and maybe not, depending on the exact situation. Another weird issue that might happen because
block
is suspending: What happens if, while
block
is in the middle of running,
triggerEvent
is called again? I would try to turn
triggerEvent
into updating some state that more accurately reflects why you want to go to a different screen. Then, the
LaunchedEffect
reads more like “I am currently showing this
Composable
, and the state I am getting from the
ViewModel
says Y. Therefore, I should navigate somewhere else, and maybe tell the
ViewModel
I did so”
r
Thanks for the detailed answer @Alex Vanyo, that definitely makes sense as to why it's an antipattern. I guess I was just trying to find a middle point between using a
Channel
or a having it all in the one state. But in saying that, don't you think it's also an anti pattern that the ui is telling the viewmodel that a state has been handled? If the ViewModel has a requirement of an "Event" to be consumed only once, but the ui never tells the ViewModel that it consumed it then you can keep consuming that even over and over again.
It also means in unit tests, following any function that would trigger an event (That would update the state), it must be followed by some
ViewModel
function that lets it know that that event has been handled.
Guess I'm just trying to find a way to make that event a true single shot event from the
ViewModel
side where it can only be consumed once
a
find a way to make that event a true single shot event from the
ViewModel
side where it can only be consumed once
That problem is really, really hard in general. An analogy that Adam Powell offered a while back that I really like is imagining a
ViewModel
as a server, and your UI as a client. It’s not as simple as just having the server tell the client to do something and guarantee it will happen. What if the network call fails? What if the client loses internet and never gets the message? What if there are multiple clients?
don’t you think it’s also an anti pattern that the ui is telling the viewmodel that a state has been handled
It’s not ideal, in the sense that
LaunchedEffect
s should be avoided in favor of hoisting state. Using this pattern for everything in your app would not scale well. But for something like navigation, which requires a call to originate from the UI (since a
navController
is UI-scoped) is when a
LaunchedEffect
makes sense. The UI is telling the viewmodel to update state after seeing previous state. And that matches up with the data flow in UDF. A different non-navigation example: Say we want to animate scrolling a list to specific place. We can’t directly do that from a `ViewModel`: The UI might not be visible, we don’t know if other animations are happening. So we could expose a
itemToScrollTo
, that the UI can act upon when it is ready, and then tell the
ViewModel
we’ve finished the scroll. The UI (if and when it exists) is a temporary “actor” in the presence of state, in the sense that it can do things as a response to user interaction or automatically. If the
ViewModel
state includes an error to show, then perhaps the UI shows a dialog waiting for the user to cancel or retry. Upon making that selection, the UI tells the
ViewModel
state that the user has given a response to that particular error (perhaps it has a unique id) Maybe instead the UI shows the error in a transient way, with something like a snackbar. Now, the UI will wait for some period of time (to give the user a chance to see it), and then update the
ViewModel
saying that particular error has been shown. One step further, the
ViewModel
is exposing some state, and the UI knows it can immediately react to it and do something, and then update the
ViewModel
to tell that it did that.
It also means in unit tests, following any function that would trigger an event (That would update the state), it must be followed by some
ViewModel
function that lets it know that that event has been handled.
Right, and I would view that as an advantage: it forces awareness that a
ViewModel
can exist without the UI available. You could write a test where multiple
ViewModel
calls happen in a row, and there still hasn’t been the call that would normally come from the UI. And to reiterate: the
ViewModel
should not be storing an “event” in-flight, that “tells” the UI to do something. It should be storing state, that can be acted upon by the UI. Admittedly, that doesn’t look that technically different, it’s more about naming and perspective
c
Uhm, in theory @Alex Vanyo response seems good. Is there any google sample project with the approach you mention?
r
Thank you so much for the detailed answer @Alex Vanyo! That clears up all the questions I had about the reasoning behind the article and just the stance of having in-flight states in a
ViewModel
, I guess it just requires some small mindset change then it won't seem weird anymore. Another thing I noticed in the Now In Android doesn't really use the state to reflect navigation but instead just does all navigation straight in the Composables (as lambdas passed to the screen and calling them onClick). Do you know of any good resource with some more examples on this? Just curious about good naming conventions/patterns. Thanks again for the answer Alex!
m
just disregard the recommended advice. A channel is best suited for the job imo. Adding the event to the state class doesn’t make any sense, and causes more problems than benefits