A teammate is using a raw Channel to back a public API stream that several callers collect. What questions do you ask to decide whether a Channel, a cold Flow, or a SharedFlow/StateFlow is the right primitive?
answer
- Three axes: multiplicity, hot/cold, state
- Every collector sees all -> SharedFlow
- Latest value on subscribe -> StateFlow
- Per-collector fresh run -> cold Flow
- Produce via Channel, expose via shareIn
basics
~20 sAsk: should every caller get every value or should values be split among them? Does the stream start fresh per caller or run once and broadcast? Is there a current value to read instantly? Those answers point to cold Flow, SharedFlow, StateFlow, or Channel.
solid answer
~40 sDecide along three axes. (1) **Multiplicity:** if every collector must see every element, a raw Channel is wrong (it fans out — single-consumption); use `SharedFlow`/`StateFlow` for broadcast or a cold `Flow` for per-collector replay. (2) **Hot vs cold:** is production independent of collectors (events already happening -> hot: Channel/SharedFlow) or should each collector trigger a fresh run (cold Flow)? (3) **State:** do callers need the latest value immediately on subscription -> `StateFlow` (conflated, always has a value). Also weigh backpressure/buffering, lifecycle ownership (who closes/cancels), and replay needs. A common refactor is: produce internally via Channel/`callbackFlow`, then expose `receiveAsFlow().shareIn(...)` as a `SharedFlow` so callers get correct multicast semantics and lifecycle.
code
kotlin · 6 lines// Wrong: raw Channel as public multi-collector stream (fan-out!)
val events: Channel<Event> // callers compete for elements
// Right: broadcast to all callers, replay last 1, lifecycle-scoped
val events: SharedFlow<Event> = source.receiveAsFlow()
.shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)go deeper
Can state the basic question of 'does everyone get every value' but may not map it to specific primitives.
Maps multiplicity and hot/cold to Channel vs Flow vs SharedFlow and knows StateFlow holds a current value.
Drives a structured decision across multiplicity, hot/cold, state, plus backpressure and lifecycle, and proposes the produce-via-Channel/expose-SharedFlow shape.
Adds API-evolution, resource/lifecycle guarantees, replay/overflow policy trade-offs and testability, and challenges whether a stream is even the right model.
## Framing: pick the primitive by required semantics Raw `Channel` as a *public* multi-caller stream is usually a smell because of **single-consumption fan-out**: each element goes to one caller. Interview-grade reasoning uses a small decision matrix. ## Axis 1 — Multiplicity (who sees each element) - **Every collector sees every element** -> broadcast: `SharedFlow`/`StateFlow`, or a cold `Flow` (each collector re-runs the producer). - **Each element handled once, split across collectors** -> fan-out: `Channel` (rarely the right *public* API; great for internal work queues). ## Axis 2 — Hot vs cold (when production runs) - **Cold (`Flow`):** production starts per `collect`, replays from scratch, nothing runs with zero collectors. Good for request/response-shaped data, DB queries, computed streams. - **Hot (`Channel`, `SharedFlow`, `StateFlow`):** production happens independently — sensor events, UI events, push updates. Late subscribers may miss earlier values unless `replay`/state holds them. ## Axis 3 — State (is there a 'current value') - **Yes, give it to me on subscribe** -> `StateFlow` (always has a value, conflates intermediate updates, distinct-until-changed by equality). - **No, just a stream of events** -> `SharedFlow` (configurable `replay`, `extraBufferCapacity`, overflow strategy). ## Secondary considerations - **Backpressure / buffering:** Channel capacity (`RENDEZVOUS`, `BUFFERED`, `UNLIMITED`, `CONFLATED`) vs SharedFlow `extraBufferCapacity` + `onBufferOverflow`. - **Lifecycle ownership:** who closes the channel / which `scope` runs `shareIn`? `SharingStarted.WhileSubscribed()` ties upstream lifetime to subscribers. - **Replay for late subscribers:** SharedFlow `replay = n`; StateFlow effectively `replay = 1` (current value). ## Typical good shape ```kotlin class Prices(scope: CoroutineScope) { private val channel = Channel<Price>(Channel.CONFLATED) // internal producer val updates: SharedFlow<Price> = channel.receiveAsFlow() .shareIn(scope, SharingStarted.WhileSubscribed(5_000), replay = 1) suspend fun publish(p: Price) = channel.send(p) } ``` Produce via Channel internally; expose a `SharedFlow` so every caller gets correct multicast + replay + lifecycle.
- Why is StateFlow not always a drop-in for SharedFlow?StateFlow conflates and requires an initial value and emits distinct values only; pure event streams where duplicates and every emission matter need SharedFlow.
- What lifecycle bug appears if you use SharingStarted.Eagerly for a rarely-collected stream?The upstream runs forever even with no subscribers, wasting resources and possibly keeping connections open; WhileSubscribed avoids that.
saying these in an interview costs you the question
- Defends raw Channel for broadcast without mentioning fan-out
- Treats StateFlow and SharedFlow as identical
- Ignores backpressure/buffer/overflow entirely
- No mention of lifecycle/scope ownership
- Picks cold Flow for already-happening hot events with no buffering thought