Radoslaw Juszczyk
04/15/2022, 8:21 AMSharedFlow
(event is sent while the UI is not collecting).Radoslaw Juszczyk
04/15/2022, 8:22 AMChannel(capacity=UNLIMITED)
seems to be just perfect for this use case. In what scenario can it lead to an event being consumed twice or lost?Landry Norris
04/15/2022, 3:48 PMLandry Norris
04/15/2022, 3:48 PMRick Clephas
04/15/2022, 4:01 PMSharedFlow
(or StateFlow
) for events. They can both emit a value multiple times (the replayCache). And a StateFlow
will actually drop values if they aren't being collected.
I would agree with @Radoslaw Juszczyk that an unlimited Channel
is the better choice. However make sure you are not collecting the channel multiple times.Rick Clephas
04/15/2022, 4:09 PMRequiring workarounds is an indication that there's a problem with these approaches. The problem with exposing events from the ViewModel is that it goes against the state-down-events-up principle of Unidirectional Data Flow.
If you're in that situation, reconsider what that one-off ViewModel event actually means for your UI and convert it to UI state. UI state better represents the UI at a given point in time, it gives you more delivery and processing guarantees, it's usually easier to test, and it integrates consistently with the rest of your app.
https://developer.android.com/jetpack/guide/ui-layer/events So the note isn't really about Channel vs SharedFlow vs StateFlow. It's actually about state vs events.
Alex Vanyo
04/15/2022, 6:18 PMUNLIMITED
Channel
, there is the potential for an element to be lost after it is received from the Channel
, but before you can do anything with it (run some code as a result).
You can try that out here: https://pl.kotl.in/pMRgpkghc. Itâs a bit of a contrived example to show the issue, but this could occur whenever you have a collector disappearing and reappearing.
In that playground, we send in 500 elements to a Channel, and then we attempt to receive those 500 elements and save them to a list. However, we also repeatedly cancel and relaunch the collector, and as a result youâll see that not all elements make it into the list.Alex Vanyo
04/15/2022, 6:38 PMChannel
from the ViewModel
to the UI.
Every once in a while, if the collector on the UI side is cancelled and restarted (such as around configuration changes) you might lose something being sent through the Channel
unless you take additional steps to guard against that.
State fundamentally doesnât have those issues. You can have the state in the ViewModel
change as much as youâd like. Even if the UI isnât present, it doesnât matter: once it is present, it will render the most recent state.
Thereâs a bit of extra work needed for figuring out how to interpret actions that the user takes as state, but the resulting logic should end up being simpler.Rick Clephas
04/15/2022, 6:54 PMviewModel.uiState.collect { uiState ->
uiState.userMessages.firstOrNull()?.let { userMessage ->
// TODO: Show Snackbar with userMessage.
// Once the message is displayed and
// dismissed, notify the ViewModel.
viewModel.userMessageShown(userMessage.id)
}
...
}
Wouldnât an update of the state cause a second snackbar (or dialog) to be shown?Alex Vanyo
04/15/2022, 7:27 PMCompose
version there is more clear on how to avoid that with a LaunchedEffect
.
As written, I think itâs implied that viewModel.userMessageShown
will immediately change the uiState
, so there wonât be an intermediate emission.
If youâre seeing another uiState
comes through (with the old userMessages
) after viewModel.userMessageShown
is called, then you could use some form of distinctUntilChangedBy
to prevent that, something like:
viewModel.uiState
.map { uiState ->
uiState.userMessages.firstOrNull()
}
.distinctUntilChangedBy { userMessage -> userMessage?.id }
.collect { userMessage ->
if (userMessage != null) {
// TODO: Show Snackbar with userMessage.
// Once the message is displayed and
// dismissed, notify the ViewModel.
viewModel.userMessageShown(userMessage.id)
}
}
Rick Clephas
04/15/2022, 7:35 PMRadoslaw Juszczyk
04/16/2022, 9:09 AMRadoslaw Juszczyk
04/16/2022, 9:22 AMRick Clephas
04/16/2022, 9:28 AMuserMessages
). Where the VM only adds messages to the list (it never replaces existing messages). The UI is then responsible for removing a message once it's shown to / dismissed by the user (the userMessageShown
call).
So in your example the two events would be added to the list during the config change. Once the activity starts collecting the state again it will receive the list with both 'events'.Radoslaw Juszczyk
04/16/2022, 9:31 AMRick Clephas
04/16/2022, 9:35 AMRadoslaw Juszczyk
04/18/2022, 9:33 AMStateFlow
approach where you store list of events in the state and remove from it once consumed I came up with a wrapper.
I ran it against that code example which @Alex Vanyo shown (showing issue with Channels) and it looks like all the events are consumed (All Sent! 500
)
Can you take a look and let me know if you dont see any obvious issues with it:
https://pl.kotl.in/cg0iXcPNYRick Clephas
04/18/2022, 1:59 PMsynchronized
you could use the update
extension function on the StateFlow
(which is available on all targets not just JVM): https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/update.html
I would also use drop
instead of subList
since drop will actually create a new list instead of a view of the current one:
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/drop.htmlRadoslaw Juszczyk
04/18/2022, 2:08 PM