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.
answer
- StateFlow = single source of truth for current state
- Requires initial value; model 'no value' explicitly
- Bad for one-shot events (dedup + conflation + replay)
- Events -> SharedFlow replay=0 / Channel
- Value-based equals + one immutable state object
basics
~10 sUse 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 sStateFlow 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
Knows StateFlow holds current state and needs an initial value.
Identifies that one-shot events are a poor fit and that an initial/loading state must be modeled.
Explains exactly why events break on StateFlow (dedup, conflation, replayed value) and reaches for SharedFlow/Channel.
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