StateFlow & SharedFlow Quiz
KOTLIN › Concurrency
A ViewModel exposes screen state. Which flow type is the idiomatic choice and why?
- SharedFlow with replay = 1, because it can cache the last emission for later
- StateFlow, because it keeps the latest value, has an initial one, and conflates
- A cold flow{} builder, because it reruns the block for each collector separately
- Channel, because it sends each item once to one collector and then removes it
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?
- StateFlow cannot emit any new values from within a coroutine body
- StateFlow silently drops all of its emissions whenever there are no collectors
- StateFlow replays its latest value to new collectors, so the event refires
- StateFlow requires onBufferOverflow set to DROP_LATEST for events
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?
- It drops the oldest buffered value to make room for the new one
- It throws a BufferOverflowException when the buffer is full
- It suspends the emitting coroutine until buffer space opens
- It silently discards the new value and returns false instead
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?
- Collection keeps running in the background, wasting work on a stopped UI
- It will not compile because collect is not a suspend function to call there
- StateFlow cannot be collected from lifecycleScope at all, only elsewhere
- It automatically stops at STOPPED, so the plain launch is already enough
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?
- It cancels both the coroutine and its upstream producer when it drops below STARTED
- It suspends below STARTED but leaves the upstream producer running, wasting work
- It is the current recommended API for collecting a StateFlow in Compose
- It guarantees the flow is collected exactly once for the whole process
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?
- vm.state.collectAsState() inside a LaunchedEffect(Unit) block
- vm.state.value read directly from the composable body on every recomposition
- vm.state.collectAsStateWithLifecycle(), collects only while STARTED
- observeAsState() from the LiveData interop API, since it works for StateFlow
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?
- It limits each single emission to a maximum of 5000 individual items
- It keeps the upstream alive 5s after the last collector unsubscribes
- It delays the very first emission by a full 5 seconds after subscribing
- It replays the last 5000 emitted values to each brand-new collector
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?
- All 100 values are delivered in order to the collector, without loss.
- It receives nothing until the producing loop finishes and stops updating.
- It may skip intermediates, but it will eventually get the latest value.
- A BufferOverflowException is thrown once the internal buffer fills up.
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?
- No new emission, because StateFlow emits only when the value is unequal
- A new emission, because every assignment notifies collectors even if unchanged
- A crash, because StateFlow forbids assigning a value that is equal to the current one
- A duplicate emission only for collectors that disabled distinctUntilChanged
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?
- Cast it at the call site with as StateFlow and read it there
- Expose it through Channel.receiveAsFlow() so callers can observe updates
- Wrap it in a new flow { } builder each time it is accessed
- Keep a private MutableStateFlow and expose it with .asStateFlow()
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?
- Wrap both assignments in runBlocking to force them to run one after another
- Use state.update { it.copy(count = it.count + 1) } for atomic retries
- Replace the StateFlow with a SharedFlow and set replay = 1 for buffering
- Annotate the assignments with @Synchronized so the .value writes stay safe
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?
- All five earlier values, delivered in the original order
- Nothing at all, because SharedFlow does not replay past items
- The last two cached values, then any later emissions
- Only the very first emitted value, before any newer ones
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?
- stateIn, because it shares the flow without requiring an initial value
- conflate(), which multicasts only the latest value out to all collectors
- buffer(64), which shares a single upstream flow among all collectors
- shareIn(scope, started, replay), a SharedFlow needing no initial value
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()?
- Eagerly starts at once and runs for the scope's life; WhileSubscribed is demand-led
- Both behave in exactly the same way at runtime, with only their operator names differing
- Eagerly replays the full emission history to collectors; WhileSubscribed replays none
- WhileSubscribed forces collection onto Dispatchers.IO; Eagerly uses Main
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).