StateFlow & SharedFlow Quiz

KOTLIN › Concurrency

A ViewModel exposes screen state. Which flow type is the idiomatic choice and why?

Answer: StateFlow, because it keeps the latest value, has an initial one, and conflates

UI state is a single always-available current value, which is exactly StateFlow's contract: required initial value, holds latest, conflated. A cold flow or single-consumer Channel does not model shared observable state.

Why is StateFlow a poor choice for emitting a one-time navigation event?

Answer: StateFlow replays its latest value to new collectors, so the event refires

A new collector (e.g. after a configuration change) immediately receives StateFlow's current value, so a navigation 'event' stored as state would be re-triggered. SharedFlow with replay = 0 avoids this.

What is the default onBufferOverflow behavior of MutableSharedFlow when emit() is called and the buffer is full?

Answer: It suspends the emitting coroutine until buffer space opens

The default policy is BufferOverflow.SUSPEND, so the suspending emit() call waits for buffer space. tryEmit() instead returns false rather than suspending.

In an Activity using Views, what is wrong with lifecycleScope.launch { vm.state.collect { ... } } with no further wrapping?

Answer: Collection keeps running in the background, wasting work on a stopped UI

Plain lifecycleScope collection runs until the scope is destroyed, so it continues while the UI is stopped. Wrapping in repeatOnLifecycle(Lifecycle.State.STARTED) cancels and restarts collection with visibility.

Which statement about launchWhenStarted is correct?

Answer: It suspends below STARTED but leaves the upstream producer running, wasting work

launchWhenStarted only pauses (suspends) the coroutine; the upstream producer keeps running, so it wastes resources. It is deprecated in favor of repeatOnLifecycle.

In Jetpack Compose, what is the recommended way to consume a ViewModel StateFlow as UI state?

Answer: vm.state.collectAsStateWithLifecycle(), collects only while STARTED

collectAsStateWithLifecycle() (from lifecycle-runtime-compose) ties collection to the lifecycle, collecting only while at least STARTED and stopping otherwise, unlike plain collectAsState() which keeps collecting in the background.

You convert a cold repository flow into UI state with stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Loading). What does WhileSubscribed(5000) achieve?

Answer: It keeps the upstream alive 5s after the last collector unsubscribes

WhileSubscribed(stopTimeoutMillis = 5000) keeps the upstream producer alive for 5s after the last subscriber unsubscribes, so a configuration change does not tear down and restart the producer.

You set MutableStateFlow.value 100 times in a tight loop while a single collector is slow. What does that collector observe?

Answer: It may skip intermediates, but it will eventually get the latest value.

StateFlow conflates: a slow collector is not guaranteed every intermediate value, only that it converges on the most recent value. There is no buffer to overflow and no waiting for completion.

A MutableStateFlow holds a UiState data class. You assign a new instance that is equal() to the current value. What happens?

Answer: No new emission, because StateFlow emits only when the value is unequal

StateFlow compares with equals (like a built-in distinctUntilChanged), so assigning an equal value produces no emission. With a data class, structurally equal content suppresses the update even though it is a new object reference.

What is the idiomatic way to expose a ViewModel's writable StateFlow so external callers can read but not emit to it?

Answer: Keep a private MutableStateFlow and expose it with .asStateFlow()

asStateFlow() returns a read-only StateFlow view over the private MutableStateFlow, preventing outside code from calling value/emit while still observing updates. A bare cast would still allow downcasting back to the mutable type.

Two coroutines concurrently run state.value = state.value.copy(count = state.value.count + 1) on a MutableStateFlow. Updates are being lost. What is the correct fix?

Answer: Use state.update { it.copy(count = it.count + 1) } for atomic retries

Read-modify-write on .value is not atomic, so concurrent updates race. MutableStateFlow.update runs the lambda inside a compareAndSet retry loop, guaranteeing each update is applied without losing the other's change.

A MutableSharedFlow is created with replay = 2. After five values have been emitted, a new collector subscribes. What does it receive first?

Answer: The last two cached values, then any later emissions

replay defines how many of the most recent emissions are cached and re-delivered to new subscribers. With replay = 2 a late collector immediately gets the last two values, then continues with new emissions.

You want to multicast an expensive cold flow to several collectors as one-off events with no natural initial value. Which operator fits?

Answer: shareIn(scope, started, replay), a SharedFlow needing no initial value

shareIn converts a cold flow into a hot SharedFlow shared across collectors and, with replay = 0, models events. stateIn requires an initial value and models state, while conflate/buffer are per-collector operators that do not multicast.

When converting a cold flow with stateIn, how does SharingStarted.Eagerly differ from SharingStarted.WhileSubscribed()?

Answer: Eagerly starts at once and runs for the scope's life; WhileSubscribed is demand-led

Eagerly begins the upstream right away and keeps it alive as long as the scope lives, regardless of subscribers. WhileSubscribed is demand-driven, starting on the first subscriber and stopping after the last unsubscribes (plus any stopTimeoutMillis).

Back to StateFlow & SharedFlow