StateFlow & SharedFlow Quiz
KOTLIN › Flow
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.
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 run this concurrently and 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
- conflate()
- buffer(64)
- shareIn
Answer: shareIn
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.
What type is MutableSharedFlow.subscriptionCount?
- A StateFlow<Int> you can observe as collectors come and go
- A suspend function returning the count when called
- A monotonically increasing total of all subscribers ever
- A configuration cap on the number of simultaneous collectors permitted
Answer: A StateFlow<Int> you can observe as collectors come and go
Being observable is the point: it lets upstream work start and stop with the subscriber count, which is exactly how SharingStarted.WhileSubscribed is implemented.
A MutableSharedFlow with replay = 0 emits while nothing is collecting. What happens to the value?
- It is dropped silently
- It is buffered until a collector subscribes
- The emit call suspends until a collector appears
- It is replayed to the next subscriber anyway
Answer: It is dropped silently
With no replay cache and no subscriber there is nowhere to hold it. Raising replay to 1 trades this for re-delivery after a rotation.
Why does a Channel with receiveAsFlow() suit one-off events better than a SharedFlow?
- It buffers when nobody listens and delivers each element exactly once
- It broadcasts each and every event to every single collector simultaneously
- It conflates so only the newest event is ever delivered
- It replays the full event history to each new collector
Answer: It buffers when nobody listens and delivers each element exactly once
That combination avoids both SharedFlow failure modes. The cost is a single collector: two would split the events between them rather than each receiving all.
What is the main limitation of using a Channel as an event bus?
- It supports one collector: two would split the events between them
- It cannot be exposed as a Flow to the UI layer
- Its emissions are conflated, so bursts of events silently lose values
- It requires a CoroutineScope at every send site
Answer: It supports one collector: two would split the events between them
A channel delivers each element to exactly one receiver, which is the property that makes it right for events and wrong whenever several independent listeners each need to see them all.
Why is StateFlow conflation specifically a problem for events?
- The same event emitted twice in a row is delivered only once
- Conflation delays each emission by one dispatcher cycle
- Conflated values are delivered on a background dispatcher instead
- Conflation discards the first value of every pair
Answer: The same event emitted twice in a row is delivered only once
Equality with the current value suppresses the emission, so the second identical snackbar never appears. For UI state that suppression is desirable, which is the whole distinction.
Which advantage does StateFlow have over LiveData?
- It is plain Kotlin, so a ViewModel using it is JVM-testable with no Android test rules
- It observes the host lifecycle automatically at the collection site, with no extra wrapping
- It guarantees delivery of every value including duplicates
- It supports multiple writers without synchronisation
Answer: It is plain Kotlin, so a ViewModel using it is JVM-testable with no Android test rules
No Android dependency means no Robolectric and no InstantTaskExecutorRule, plus the whole operator library. Lifecycle awareness is the thing it gives up.
Which advantage does LiveData retain over StateFlow?
- observe(owner) is lifecycle-aware without any extra work
- It conflates duplicate consecutive values, which StateFlow does not
- It always holds a non-null current value
- It offers a richer set of stream operators
Answer: observe(owner) is lifecycle-aware without any extra work
That is the trade the newer API made: more power, less safety by default. Both conflate, and StateFlow is the one with the guaranteed value and the operators.
What does resetReplayCache() accomplish?
- Clears replayed values so a new subscriber receives none, without emitting anything
- Emits a null value to every current subscriber
- Resets the flow to its initial value
- Cancels all of the current subscriptions so that they each have to resubscribe fresh
Answer: Clears replayed values so a new subscriber receives none, without emitting anything
It is the workaround for a replay-1 event being re-delivered after a rotation. StateFlow has no equivalent, because it must always have a current value.