Flow & Channels
Asynchronous streams in kotlinx.coroutines — cold Flow versus hot SharedFlow and StateFlow, operators, backpressure with buffer, conflate and collectLatest, why flowOn only affects upstream, and when a Channel beats a Flow.
11 questions
JuniorTheoryVery commonHow does a cold Flow differ from a hot SharedFlow?
How does a cold Flow differ from a hot SharedFlow?
A cold Flow does nothing until it is collected and re-runs its producer separately for every collector, so each collector gets its own private sequence. A SharedFlow is hot: it emits whether or not anyone is listening, and all its collectors share one stream of emissions from a single producer.
Common mistakes
- ✗Thinking a cold
Flowcaches its values and replays them to later collectors - ✗Expecting a
SharedFlowto stop emitting when nobody is collecting it - ✗Assuming two collectors of a cold
Flowshare one run of the producer
Follow-up questions
- →How would you turn a cold
Flowinto a hot one shared by all collectors? - →What does a
SharedFlowdo with values emitted before the first subscriber?
JuniorTheoryVery commonWhen do intermediate operators such as map and filter actually run on a Flow?
When do intermediate operators such as map and filter actually run on a Flow?
Never at declaration time. An intermediate operator such as map, filter or onEach only returns a new Flow that wraps the previous one, so building the chain does no work at all. The whole chain runs when a terminal operator — collect, first, toList — starts it, once per collection.
Common mistakes
- ✗Believing an operator chain does work before a terminal operator runs
- ✗Thinking each operator gets its own coroutine or thread
- ✗Expecting
collectto reuse results computed by a previous collection
Follow-up questions
- →Which operators are terminal, and what does each of them do to the chain?
- →How does
onEachdiffer fromcollectif both look at every value?
JuniorTheoryCommonWhat does a Flow give you that a suspend function returning one value cannot?
What does a Flow give you that a suspend function returning one value cannot?
A suspend function runs once and hands back a single value. A Flow emits many values over time, and the collector handles each one as it arrives. A Flow is cold: the producer block runs only when a terminal operator such as collect is called, and it starts over for every collector.
Common mistakes
- ✗Thinking a cold
Flowproduces values before anyone collects it - ✗Believing the producer runs once and is then shared between all collectors
- ✗Treating a
Flowas a pre-computed collection instead of a stream over time
Follow-up questions
- →What happens when two collectors collect the same cold
Flow? - →Which terminal operators besides
collectstart aFlow?
MiddleTheoryCommonHow does a Channel deliver its elements when two coroutines receive from it?
How does a Channel deliver its elements when two coroutines receive from it?
A Channel is a hot queue with suspension: send suspends while the buffer is full and receive suspends while it is empty. Every element goes to exactly ONE receiver — two receivers fan the work out between them rather than each getting a copy. That is what makes it the tool for producer/consumer handoff.
Common mistakes
- ✗Expecting a
Channelto broadcast a copy of each element to every receiver - ✗Thinking a
Channelis cold and starts a fresh producer per receiver - ✗Forgetting that
sendsuspends on a full buffer instead of dropping elements
Follow-up questions
- →How do
RENDEZVOUS,BUFFEREDandCONFLATEDcapacities change whatsenddoes? - →What happens to a suspended
receivewhen the channel is closed or cancelled?
MiddleDebuggingCommonWhy does the UI still freeze after the repository flow adds flowOn(Dispatchers.IO)?
Why does the UI still freeze after the repository flow adds flowOn(Dispatchers.IO)?
flowOn changes the context only for operators UPSTREAM of it — the producer and everything declared before the call. The collector always runs in the context of the coroutine that called collect, because Flow preserves context. So parseHeavy inside collect stays on the main thread; move it upstream, above flowOn.
Common mistakes
- ✗Believing
flowOnmovescollectoff the calling dispatcher - ✗Thinking
flowOnapplies to operators placed after it in the chain - ✗Doing heavy CPU work inside the collect block on the main dispatcher
Follow-up questions
- →What does context preservation mean for a
Flow, and why is it enforced? - →What would two
flowOncalls in one chain do to the operators between them?
MiddlePerformanceOccasionalHow do buffer, conflate and collectLatest each handle a producer faster than its collector?
How do buffer, conflate and collectLatest each handle a producer faster than its collector?
By default collection is sequential: a slow collector back-pressures the producer, so emit suspends. buffer runs the producer and the collector in separate coroutines with a queue between them. conflate keeps only the newest value and drops the ones in between. collectLatest cancels the collector's body on each new value and restarts it.
Common mistakes
- ✗Thinking
conflateslows the producer down instead of dropping intermediate values - ✗Expecting
collectLatestto finish the previous collector body rather than cancel it - ✗Assuming a
Flowbuffers by default instead of suspendingemiton a slow collector
Follow-up questions
- →What does
buffer(onBufferOverflow = DROP_OLDEST)change compared with plainconflate? - →Why must the body collected by
collectLatestbe cancellable for it to help at all?
MiddleDebuggingOccasionalWhy does the second identical error toast never reach the UI through this StateFlow?
Why does the second identical error toast never reach the UI through this StateFlow?
StateFlow conflates and behaves as if distinctUntilChanged were applied, so assigning a value equal to the current one emits nothing — the second identical Toast is dropped before any collector sees it. StateFlow models state, not events: publish them through a MutableSharedFlow and emit each one.
Common mistakes
- ✗Using
StateFlowfor events and blaming the collector when a duplicate disappears - ✗Forgetting that
StateFlowdrops a value equal to the one it already holds - ✗Patching the loss with a manual consumed-flag instead of a
SharedFlowfor events
Follow-up questions
- →Why does a
MutableSharedFlowwithreplay = 0deliver two equal events? - →What would
extraBufferCapacity = 0do to anemitwhen the collector is slow?
MiddleTheoryOccasionalWhen should a stream be exposed as a Flow rather than as a Channel?
When should a stream be exposed as a Flow rather than as a Channel?
Expose a Flow whenever every collector should see the whole stream and the producer should start on demand: it is cold, declarative and re-runs per collector, so nothing leaks if nobody collects. Reach for a Channel only for a hot handoff where each element must reach exactly one consumer — a work queue between coroutines.
Common mistakes
- ✗Exposing a hot
Channelfrom a repository where a coldFlowwould do - ✗Believing a
Channelcopies each element to every receiver - ✗Forgetting that an uncollected
Channelkeeps its producer and buffer alive
Follow-up questions
- →How does
receiveAsFlowdiffer fromconsumeAsFlowwhen two collectors appear? - →Which of the two would you pick to distribute jobs across a pool of workers, and why?
SeniorDesignRareA repository must expose observeArticles(): Flow<List<Article>> to one screen. The screen has to show cached data from the local database immediately, then refresh from the network and show the updated list without re-subscribing. A network failure must leave the cached list on screen, with the error surfaced separately. Rotating the device must not fire a second network call for the same data, and the emitted list must never lag behind the database. Describe the design: where the emitted data comes from, what triggers the refresh, what carries the error, and how the stream stays a single source of truth.
A repository must expose observeArticles(): Flow<List<Article>> to one screen. The screen has to show cached data from the local database immediately, then refresh from the network and show the updated list without re-subscribing. A network failure must leave the cached list on screen, with the error surfaced separately. Rotating the device must not fire a second network call for the same data, and the emitted list must never lag behind the database. Describe the design: where the emitted data comes from, what triggers the refresh, what carries the error, and how the stream stays a single source of truth.
Make the database the single source of truth: return its query Flow, so every write re-emits the list and the UI never lags behind storage. Trigger the refresh from the stream itself and write the network result INTO the database instead of emitting it. Failures go to a separate error stream, so a failed refresh leaves the cached list untouched. Share it with stateIn(WhileSubscribed).
Common mistakes
- ✗Emitting the network result directly instead of writing it into the database
- ✗Letting a refresh failure tear down the stream and wipe the cached list
- ✗Re-collecting a cold flow on rotation and firing a duplicate network call
Follow-up questions
- →Why does
WhileSubscribedwith a small timeout survive a rotation but not a real exit? - →How would you tell the screen that a refresh is in flight without touching the list?
SeniorTheoryRareWhat does the catch operator actually catch, and which failure does it miss?
What does the catch operator actually catch, and which failure does it miss?
catch is upstream-only: it sees exceptions from the producer and from the operators declared BEFORE it, and nothing else. An exception thrown inside collect is downstream, so catch never sees it and it propagates to the caller — that is exception transparency. Use retry/retryWhen for transient upstream failures and a plain try/catch around collect.
Common mistakes
- ✗Expecting
catchto handle an exception thrown inside thecollectblock - ✗Placing
catchbefore the operator whose failure it was meant to handle - ✗Using
catchwhere the failure is transient andretry/retryWhenis the right operator
Follow-up questions
- →Why does
catchafter amapsee themap's exception but acatchbefore it does not? - →How would you emit a fallback value from inside
catchinstead of rethrowing?