Why does the second identical error toast never reach the UI through this StateFlow?
A view model publishes one-shot UI events through a StateFlow. The first failed save shows a toast. The user retries, the save fails again with the very same message — and no toast appears at all. The collector is alive and other screens update fine.
Constraint: the fix must keep every event, including two identical ones in a row, and must not introduce a manual "consumed" flag on the event object.
class SaveViewModel : ViewModel() {
private val _events = MutableStateFlow<UiEvent?>(null)
val events: StateFlow<UiEvent?> = _events
fun onSaveFailed() {
_events.value = UiEvent.Toast("Save failed")
}
}
Find and fix the bug.
StateFlow conflates and behaves as if distinctUntilChanged were applied, so assigning a value equal to the current one emits nothing — the second identical Toast is dropped before any collector sees it. StateFlow models state, not events: publish them through a MutableSharedFlow and emit each one.
- ✗Using
StateFlowfor events and blaming the collector when a duplicate disappears - ✗Forgetting that
StateFlowdrops a value equal to the one it already holds - ✗Patching the loss with a manual consumed-flag instead of a
SharedFlowfor events
- →Why does a
MutableSharedFlowwithreplay = 0deliver two equal events? - →What would
extraBufferCapacity = 0do to anemitwhen the collector is slow?
What happens
MutableStateFlow is a conflated state holder. Its value setter compares the new value with the old one via equals and, if they are equal, emits nothing: a StateFlow behaves as if distinctUntilChanged were always applied to it.
UiEvent.Toast("Save failed") is a data class, so the second Toast with the same text equals the first. The assignment goes through, no emission happens, no toast.
The fix
class SaveViewModel : ViewModel() {
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onSaveFailed() {
_events.tryEmit(UiEvent.Toast("Save failed"))
}
}
A SharedFlow keeps no current value and never compares emissions, so two equal events are both delivered. replay = 0 keeps the event from replaying after a configuration change.