skip to content

Design-wise, when is StateFlow the right state-holder and when is it a poor fit? Address null handling, the always-present value, and event delivery.

level: principalimportance: nice to knowfreq 35%

answer

  1. StateFlow = single source of truth for current state
  2. Requires initial value; model 'no value' explicitly
  3. Bad for one-shot events (dedup + conflation + replay)
  4. Events -> SharedFlow replay=0 / Channel
  5. Value-based equals + one immutable state object

basics

~10 s

Use StateFlow when there's always a meaningful current state to read. It's a poor fit for one-time events, because equal values are skipped and intermediate values can be dropped.

solid answer

~40 s

StateFlow fits any 'single source of truth for current state' — UI state, selected item, connection status — because it always has a value, dedups equal updates, and lets observers read .value synchronously. It requires an initial value (so model 'no value yet' explicitly, e.g. StateFlow<UiState> with a Loading/Empty case, or StateFlow<T?> with null). It's a poor fit for one-shot events (navigation, snackbars, errors-to-show-once): equality dedup drops repeats (two identical errors emit once), conflation can drop fast bursts, and the replayed current value re-delivers the last event to new collectors. For events use a SharedFlow with replay=0 (or a Channel). Other considerations: StateFlow.collect never completes, so scope it to a lifecycle; equality dedup demands value-correct equals on the state type; and a single immutable data-class state updated via update { } keeps consistency.

go deeper

for a junior

Knows StateFlow holds current state and needs an initial value.

for a middle

Identifies that one-shot events are a poor fit and that an initial/loading state must be modeled.

for a senior

Explains exactly why events break on StateFlow (dedup, conflation, replayed value) and reaches for SharedFlow/Channel.

for a principal

Designs the state/event split across an app, enforces value-based equals and a single immutable state object, and justifies the primitive choice per use case.

## When StateFlow is the right tool StateFlow models **current state** — there is always a meaningful 'now' value: - UI/screen state in a ViewModel (`StateFlow<UiState>`). - Selected tab, current user, connectivity status, form contents. - Anything an observer should be able to read synchronously via `.value` and re-render from on subscribe. Properties that make it good here: always-present value, replay-of-1 so late subscribers are correct, equality dedup so identical states don't cause redundant re-renders, conflation so only the latest matters. ## The always-present-value constraint StateFlow **requires an initial value** — `MutableStateFlow(initial)`. So 'no value yet' must be modeled explicitly: ```kotlin sealed interface UiState { data object Loading : UiState data class Ready(val items: List<Item>) : UiState } val state = MutableStateFlow<UiState>(UiState.Loading) ``` Or use a nullable type `MutableStateFlow<User?>(null)` when null genuinely means 'absent'. There's no 'empty StateFlow.' ## Where StateFlow is a poor fit: one-shot events For events that should fire exactly once — navigation, a toast, showing an error — StateFlow breaks down: - **Equality dedup**: emitting the same error twice (`ShowError("x")` then `ShowError("x")`) is deduplicated to a single emission — the second 'event' is lost. - **Conflation**: a burst of events keeps only the latest; intermediate events vanish. - **Replayed current value**: a new collector immediately re-receives the last event, re-triggering it (e.g. re-navigating after rotation). The idiomatic fix is a `SharedFlow` with `replay = 0` (or a `Channel` consumed once). (Those primitives are siblings to this topic.) ## Other design notes - `collect` never completes, so always collect inside a lifecycle/scope that cancels it. - Equality dedup means the state type needs a **value-based equals** (use a `data class`); otherwise identical-looking states still re-emit. - Prefer one immutable state object updated atomically with `update { it.copy(...) }` over many separate StateFlows, for a consistent snapshot. ## Summary StateFlow = the latest state, always readable, dedup'd. Not an event channel. Choosing it for one-shot signals is the classic misuse.

  • Why does StateFlow re-trigger a one-time navigation event after a config change?
    A new collector receives the replayed current value (replay=1), so the last 'event' is delivered again, re-firing navigation.
  • How do you represent 'loading, no data yet' with StateFlow?
    Give it an explicit initial state — e.g. a sealed Loading case or a nullable type initialized to null — since StateFlow always has a value.

saying these in an interview costs you the question

  • Using StateFlow as a one-shot event bus
  • Expecting repeated identical events to all fire (dedup drops them)
  • Forgetting StateFlow needs an initial value
  • Splitting state into many StateFlows losing snapshot consistency
  • Ignoring that the replayed value re-delivers the last 'event' to new collectors

context