You must build a derived UI state from three independent flows and also fan in raw events from many sources. Which combining operators do you choose and why, and what pitfalls do you guard against?
answer
- combine for latest-of-each derived state
- merge for same-type event fan-in
- Seed sources + stateIn for hot cached state
- zip is wrong: lockstep, drops extras
- Guard: conflation, non-determinism, failure-cancels-siblings
basics
~20 sUse combine to build one UI state from the three inputs because it reacts to the latest of each. Use merge to pool the many same-type event flows into one stream. Avoid zip here — you don't want lockstep pairing.
solid answer
~40 sFor derived UI state from three independent inputs, combine(flowA, flowB, flowC) { a, b, c -> UiState(...) } is the right tool: it recomputes on any input change using the latest of each, which matches 'state should reflect newest of everything'. Seed sources that may not emit yet (onStart { emit(default) } or use StateFlow) so combine produces an initial state; otherwise it stays silent. For fanning in many homogeneous event flows, merge(*flows) (or listOf(flows).merge()) pools them with no transform. zip is wrong here — lockstep pairing couples rates and drops extras. Pitfalls: combine's conflation can skip intermediates (fine for state, bad for accounting); merge's order is non-deterministic; one source's failure cancels the rest; and expose the result as StateFlow via stateIn for a hot, cached, replayable state.
code
kotlin · 9 linesimport kotlinx.coroutines.flow.*
// A) derived state: latest-of-each -> combine, exposed as StateFlow
val uiState: StateFlow<UiState> =
combine(user, cart, network) { u, c, n -> UiState(u, c.items, n.isOnline) }
.stateIn(scope, SharingStarted.WhileSubscribed(5_000), UiState.Empty)
// B) event fan-in: same type, no transform -> merge
val events: Flow<AppEvent> = merge(clicks, swipes, deepLinks)go deeper
Picks combine for the merged state and merge for pooling events, at a basic level.
Justifies the choices, seeds sources for an initial state, and avoids zip for this use case.
Designs the full pipeline with stateIn/SharingStarted, handles conflation and per-source resilience, and reasons about merge's non-determinism.
Weighs correctness (state vs. event accounting), failure isolation across sources, sharing/lifecycle trade-offs, and testability under virtual time.
## Two sub-problems, two operators ### A. Derived UI state from 3 independent inputs -> `combine` The requirement 'state reflects the **latest** of every input' is exactly `combine`'s contract: it recomputes whenever **any** source emits, using each source's **most recent** value. ```kotlin val uiState: StateFlow<UiState> = combine(user, cart, network) { u, c, n -> UiState(user = u, items = c.items, online = n.isOnline) }.stateIn(scope, SharingStarted.WhileSubscribed(5_000), UiState.Empty) ``` Key decisions: - **Seed missing sources** with `onStart { emit(default) }`, or model them as `StateFlow` (always has a value) so `combine` can emit an initial state instead of staying silent until all three have fired. - **Hot, cached state:** wrap with `stateIn(...)` to turn the cold `combine` into a `StateFlow` that caches the latest value and shares one upstream across collectors. - **Intermediate states:** if you need a `Loading` phase, use `combineTransform { emit(Loading); emit(Ready(...)) }`. ### B. Fan in many homogeneous event flows -> `merge` ```kotlin val events: Flow<AppEvent> = merge(clicks, swipes, deepLinks) // all Flow<AppEvent> ``` `merge` pools same-type streams with **no transform**, forwarding each as it arrives. If the sources differ in type, `map` each into a common `sealed interface AppEvent` first, then merge. ## Why not `zip` here? `zip` pairs by index and stalls the fast side waiting for the slow side, dropping trailing items when one completes. Neither 'latest-state' nor 'pool all events' wants that. Reserve `zip` for genuinely index-correlated data. ## Pitfalls to guard against - **combine conflation:** intermediate values of a bursty source can be skipped. Acceptable for derived **state**; wrong for **event accounting** (use `merge`/explicit buffering there). - **merge non-determinism:** output order is arrival order; don't rely on it. Test with virtual time, not wall clock. - **Structured-concurrency failure:** an error in **one** source cancels the **others** and propagates. Add per-source `catch`/`retry` upstream if a source should be resilient. - **Lifecycle/leaks:** use `stateIn`/`shareIn` with `SharingStarted.WhileSubscribed` so collection stops when nobody listens. ## Keywords/APIs `combine`, `combineTransform`, `merge`, `stateIn`, `shareIn`, `SharingStarted.WhileSubscribed`, `StateFlow`, `onStart`, `catch`, `retry`.
- Why wrap the combine result in stateIn instead of exposing the cold flow?stateIn makes it a hot StateFlow that caches the latest value and shares one upstream across collectors, avoiding re-running the combine per subscriber and giving consumers an immediate current value.
- One event source occasionally throws. How do you keep the merged stream alive?Add catch/retry on that individual source before merging, so its failure is handled upstream rather than cancelling the whole merged flow via structured concurrency.
combine is the cockpit instrument that fuses the latest of every gauge; merge is the intercom where any crew member can speak into one channel.
saying these in an interview costs you the question
- Reaching for zip to build latest-state UI
- Forgetting to seed sources so combine never emits an initial state
- Exposing a cold combine flow as shared state without stateIn/shareIn
- Relying on a deterministic order from merge
- Not accounting for one source's failure cancelling the rest