Reading the guide to app architecture, I found thi...
# compose
a
Reading the guide to app architecture, I found this note:
Copy code
In some apps, you might have seen ViewModel events being exposed to the UI using Kotlin Channels or other reactive streams. These solutions usually require workarounds such as event wrappers in order to guarantee that events are not lost and that they're consumed only once.
I too (sometimes) use a
Channel
to send events from ViewModel to UI as a
Flow
which I collect in
LaunchedEffect
but I never had to use any
workarounds such as event wrappers in order to guarantee that events are not lost and that they're consumed only once
It is automatically taken care of while using channels. What exactly did the author mean here?
a
It's referring to some patterns that were mostly only seen alongside LiveData, since LiveData is a state holder and not an event stream. It was sort of a workaround for some other design issues involved with multiple consumer handoff during android activity recreation. If you haven't seen this before, you're not missing out. 🙂
a
I remember the
LiveData<Event<T>>
pattern which was used as a workaround to guarantee that events are consumed only once. But that wasn't in anyway related to channels. The guide specifically refers to Kotlin Channels (and similar reactive streams). Is there any such thing with Channels that I need to watch out for?
h
First thing comes to mind is possibility of using something like
BroadcastChannel
(now deprecated?) which replays some items to new subscribers.
m
some
Flow
configurations as well
if you've got a channel then I'm pretty sure it guarantees exactly-once delivery though? especially with rendezvous (e.g. no buffer)
a
@madisp Yeah, right. Never faced an issue with this approach.
a
https://github.com/Kotlin/kotlinx.coroutines/issues/2886 has some more information in this area. The edge case specifically with a
Channel
is that an element can be received from a
Channel
, but the receiver is cancelled before the element is processed, which results in behavior of “at-most-once delivery and handling” More general solutions that get closer to that “exactly-once delivery and handling” start getting extremely subtle, can depend on dispatcher behavior, and can break due to depending on a very precise dance of ordering (things like adding intermediate filtering or mapping can break assumptions). It’s possible you’ve avoided those edge cases so far if “at-most-once” is good enough for your use case or if your usage happens to avoid the issue. Just to re-iterate again, exposing state from a ViewModel (either
StateFlow
or Compose
State>
) is the simplest to reason about, test, and should be your go-to approach.
s
It is actually a bit unfortunate that the discussion over at https://github.com/Kotlin/kotlinx.coroutines/issues/2886 didn’t seem to continue and instead the suggestion became to bake events inside the State itself, with the dance needed to: • Do whatever action on the ui, send that action to the VM • Depending on that action, update the state to contain a relevant event • Receive the new state, extract the event out of it, process it and then manually ask the VM to set the event as consumed • Have the VM reset the event, again changing the view state. It may be that I find it less than ideal because I am not used to it, but it certainly feels like quite a lot of ceremony over what a channel of events let us do.
a
bake events inside the State
Just to highlight an important mindset here which is fairly subtle: Try to go one step further than just putting the events into a part of the state and thinking about them as events the entire time. So instead of
List<Message>
“here are the list of show message events that haven’t been sent to the UI yet”, think “here are a list of messages pending display to the user”. The underlying data being stored will be very similar, which admittedly makes this a subtle distinction. When you take the mindset of “here are a list of messages pending display to the user,” there’s a bunch of natural follow-up questions that are important: • When is a message considered “displayed” to remove it from the list? • Should the list of messages have a max size if the list keeps growing? • Should the list of messages be stored (for example, to persist through process death)? An approach with a channel with events hide those questions. For example, what happens if you receive an event from a channel just before a configuration change happens? Or the user backgrounds the app 10 milliseconds after starting to show the message? Those questions would still be valid even with a different primitive than a
Channel
. The “dance,” as you put it, between the ViewModel and the UI is a bit longer, but it makes the answers to the above questions for your use case explicit and not depend on the exact internal workings of
Channel
or another primitive. It doesn’t hide the reality that an AAC ViewModel outlives the UI, and that guaranteeing a longer-lived producer telling a shorter-lived consumer to do something exactly once is very difficult.
s
Very valid points raised. I guess I am mostly salty about the fact that there is no super easy solution to this, and I am definitely not used to handling events as you’re describing. I think that this may be one of those things that just need a bit of time to settle. Like how coroutines made no sense in the beginning. Or how declarative UI doesn’t make sense the first months. I think this scenario is one of those cases, and provided enough time to play around with it, seeing examples and the community discuss it, it will all make much more sense eventually.
a
@Alex Vanyo Btw what is your opinion on putting the
SnackbarHostState
in the ViewModel itself and calling
showSnackbar
right from the ViewModel?
a
Fwiw I have a few hobby apps that put a snackbar state object like that all the way into an app/singleton scope. The benefit of reducing to state in this way is that it doesn't matter how many things are observing the state at the same time, whether that's zero or many.