One-off events from a ViewModel

Flow and StateFlow Senior Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Pick the fix

The 'Saved' snackbar sometimes never appears when the user rotates during the save. What's the most robust fix?

class EditorViewModel : ViewModel() {
    private val _events = MutableSharedFlow<String>()
    val events = _events.asSharedFlow()

    fun save() = viewModelScope.launch {
        saveToDisk("draft")
        _events.emit("Saved")
    }
}
Options
Hint 1 of 1

What happens to a SharedFlow value when nobody is collecting?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A Put the message in UI state and clear it after the UI shows it

Correct. State survives rotation and is only cleared once handled.

Why not the others?
  • B Add replay = 1 to the SharedFlow. Now the snackbar shows again for every new collector, including after rotation.
  • C Emit the event twice. That shows the message twice when nothing is lost.
  • D Use GlobalScope to emit. The scope that emits isn't the problem; the stream drops values with no collector.

30-second answer

The current Android guidance is to model events as state: put the message in UI state, show it, then tell the ViewModel it was handled so it clears it. That survives rotation and can't be lost. If you use an event stream, a buffered Channel received with receiveAsFlow delivers each event to one collector and holds events while nobody listens.

Practise saying it · 40 s. No recording, just a timer

Explain how you deliver one-off events.

Say your answer out loud, then check which of these points you covered:

  • Prefer events as state, cleared after handling
  • SharedFlow without replay drops events
  • Channel buffers for one collector
  • Easy to test

Deep dive

A SharedFlow(replay = 0) drops events emitted while the UI is stopped, for example during a rotation.

A Channel can still lose an event if the collector is cancelled after receiving and before handling it; collecting on Dispatchers.Main.immediate narrows that window.

State-based events make tests simple: assert on the state, call the 'handled' function, assert again.

Navigation after a successful save is often better as state too, like saved = true, which the UI reacts to.

Events as state Compile-checked · Kotlin 2.4.21
data class EditorState(val text: String = "", val message: String? = null)

class EditorStateViewModel : ViewModel() {
    private val _state = MutableStateFlow(EditorState())
    val state = _state.asStateFlow()

    fun save() = viewModelScope.launch {
        saveToDisk(_state.value.text)
        _state.update { it.copy(message = "Saved") }
    }

    fun onMessageShown() = _state.update { it.copy(message = null) }
}

Common mistakes

  • Putting the event in a StateFlow and never clearing it, so it repeats after rotation.
  • Using SharedFlow(replay = 0) and losing events while the screen is stopped.
  • Sending events from the UI back into the ViewModel through the same stream.

Senior take & follow-ups

Seniors know both camps and pick state for most cases, giving the rotation and lost-event reasons.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why is a Channel better than SharedFlow here?

    Show hint

    It buffers until a collector receives, and each event goes to one collector.

  2. How do you test a state-based event?

    Show hint

    Assert the message is in state, call onMessageShown, assert it's gone.

Suggest a fix if something here is wrong or out of date.