skip to content

You are designing a ViewModel exposing UI state and one-off events. How do you decide between stateIn, shareIn, and a MutableStateFlow/MutableSharedFlow, and what scope and SharingStarted do you pick?

level: principalimportance: should knowfreq 40%

answer

  1. Current value? -> StateFlow; stream of events -> SharedFlow
  2. Cold upstream -> stateIn/shareIn; you push -> Mutable*Flow + asStateFlow()
  3. UI state: stateIn(viewModelScope, WhileSubscribed(5000), Loading)
  4. Events: SharedFlow replay 0 / Channel, never StateFlow
  5. App-wide state: own a scope, Eagerly/Lazily

basics

~10 s

Use stateIn for continuous UI state derived from a flow, shareIn for shared event streams, and a manual MutableStateFlow/SharedFlow when you push values yourself. Host them in viewModelScope and usually start with WhileSubscribed(5000).

solid answer

~40 s

Decide by data semantics. Continuous, always-has-a-value UI state derived from an upstream (repository flow): use stateIn(viewModelScope, WhileSubscribed(5000), initialValue = Loading) so the state survives rotation, stops the upstream when the screen is gone, and exposes .value to Compose. A shared multi-collector event/data stream that has no single 'current value' and may have multiple consumers: shareIn with an appropriate replay (0 for events). When the ViewModel itself emits (button clicks, navigation): hold a private MutableStateFlow/MutableSharedFlow and expose its read-only asStateFlow()/asSharedFlow() — stateIn/shareIn are only for converting an existing cold upstream, not for imperative pushes. Scope is almost always viewModelScope (cancelled with the ViewModel). For events, prefer SharedFlow with replay 0 and extraBufferCapacity, or a Channel, because StateFlow conflation/de-dup drops duplicate or rapid events. WhileSubscribed(5000) is the default; Eagerly only for cheap always-on state.

go deeper

for a junior

Knows UI state goes in a StateFlow and events are different; can use stateIn with viewModelScope from a template.

for a middle

Distinguishes stateIn (cold upstream) from MutableStateFlow (imperative) and picks WhileSubscribed(5000) for UI.

for a senior

Correctly routes events to SharedFlow/Channel, exposes read-only views, and justifies scope and SharingStarted per case.

for a principal

Designs the whole state/event architecture, owns lifecycle/scope decisions for app-wide vs screen-scoped state, and reasons about conflation correctness and resource cost trade-offs.

## The decision axes 1. **Is there a meaningful 'current value'?** Yes -> StateFlow. No (it's a stream of events) -> SharedFlow. 2. **Where does data come from?** An existing cold upstream you transform -> use `stateIn`/`shareIn`. The ViewModel pushes values imperatively -> use a `Mutable*Flow` you write to. 3. **Lifetime/cost?** -> choose `SharingStarted`. ## UI state (the common case) ```kotlin val uiState: StateFlow<UiState> = repository.observe() .map(::toUiState) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = UiState.Loading, ) ``` - `stateIn` because UI always needs a current value (`.value`) and conflation is desirable (only latest matters). - `viewModelScope` so the shared upstream dies with the ViewModel. - `WhileSubscribed(5000)` to stop the upstream when the screen is gone but survive rotation. ## Imperative state you set yourself ```kotlin private val _query = MutableStateFlow("") val query: StateFlow<String> = _query.asStateFlow() fun onQueryChange(q: String) { _query.value = q } ``` `stateIn` does not apply — there is no cold upstream; you are the producer. Expose a **read-only** view via `asStateFlow()`. ## One-off events StateFlow is the **wrong** tool for events: it conflates and de-dups, so two identical 'show snackbar' events or rapid events get lost, and a late subscriber re-receives the last one. Prefer: ```kotlin private val _events = MutableSharedFlow<Event>(replay = 0, extraBufferCapacity = 1) val events: SharedFlow<Event> = _events.asSharedFlow() ``` or a `Channel` consumed as `receiveAsFlow()`. If you are *converting* a cold event upstream for multiple collectors, `shareIn(scope, WhileSubscribed(5000), replay = 0)` is appropriate. ## Combining flows into state ```kotlin val state = combine(flowA, flowB) { a, b -> merge(a, b) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), Initial) ``` ## Scope choices - ViewModel: `viewModelScope`. - Application-wide singleton state: a custom `CoroutineScope(SupervisorJob() + Dispatchers.Default)` you own and never cancel; here `Eagerly` or `Lazily` is common. - Never use `GlobalScope`. ## SharingStarted recap for this design - `WhileSubscribed(5000)`: UI-bound, default. - `Eagerly`: cheap, always-needed warm state. - `Lazily`: start on demand, keep warm; rare for UI.

  • Why not expose events as a StateFlow?
    StateFlow conflates and de-duplicates and always has a current value, so identical or rapid events are dropped and late subscribers re-receive the last event — wrong semantics for one-off events.
  • When would you NOT use stateIn even for state?
    When you produce the value imperatively (no cold upstream) — then use a MutableStateFlow you set directly and expose via asStateFlow().
  • What scope for application-wide cached state that must outlive any screen?
    A long-lived, app-owned CoroutineScope(SupervisorJob() + a dispatcher) — not viewModelScope, not GlobalScope — typically with Eagerly or Lazily.

saying these in an interview costs you the question

  • Using stateIn to push imperative values you set yourself
  • Exposing events through a StateFlow
  • Using GlobalScope for the sharing scope
  • Exposing the mutable flow directly instead of asStateFlow()/asSharedFlow()
  • Defaulting to Eagerly for expensive UI-bound upstreams

context