Flow Context, Errors & Backpressure Interview Questions
KOTLIN › Flow
A colleague wraps emit in withContext(Dispatchers.IO) inside a flow builder and gets a runtime crash. Explain the error and the correct fix.
What a strong answer covers: The error is IllegalStateException with the message that the flow invariant is violated, saying the flow was collected in one context but emission happened in another. It exists to protect context preservation, which is the guarantee that a collect lambda runs in whatever context called collect. That guarantee is what lets a ViewModel collect on Main and assign to state without wrapping anything, and it would be worthless if a flow could quietly deliver on a pool thread instead. So the library checks and throws rather than trusting the author. The fix is flowOn, which moves the producer declaratively: put the work in the builder, then apply flowOn(Dispatchers.IO) below it, and everything above that call runs on IO while the collector stays where it was. The mental model I would offer is that withContext is imperative and scoped to a block, whereas a flow needs the context decision to be part of the chain so the library can reason about it. There is one legitimate case where you genuinely need to emit from a different coroutine, for instance merging values produced by several concurrent children, and for that the answer is channelFlow rather than flow, because it is backed by a channel and therefore does not carry the restriction.
How would you decide between buffer, conflate, and collectLatest for a given screen?
What a strong answer covers: I would ask two questions: can I afford to lose values, and is the collector work cancellable. buffer is for when I cannot lose anything but want the producer and collector to overlap rather than run in lockstep. It queues, and only suspends the producer once the queue is full, so it is a throughput optimisation with the default no-loss semantics intact. Writing a stream of rows to a database while reading from the network is the shape. conflate is for when only the newest value has meaning and the collector work is cheap and atomic, like rendering a position or a sensor reading. It never suspends the producer and silently drops intermediates, but crucially it lets a slow collect block finish, so the block is never interrupted partway. collectLatest is for when the collector work is expensive and cancellable and only the newest input matters, the canonical case being a search box where each new query should abandon the in-flight request. It does not drop values, it delivers them and cancels the block still working on the previous one. The trap I would mention is fusion: conflate is literally buffer(1, DROP_OLDEST), so if someone later appends a plain buffer(50) the two merge into one configuration, the later one wins, and the conflation silently disappears. If you want a large queue that also conflates you have to say buffer(50, BufferOverflow.DROP_OLDEST) explicitly.
Where do you put catch, retry and onCompletion in a repository flow, and why does the order matter?
What a strong answer covers: Order matters because catch has a strictly upstream field of view: it sees the producer and every operator above it, and nothing below. So catch goes last, or at least below everything I want it to cover. If I put onStart above catch, then a failure inside onStart is also caught; if I put it below, it is not. retry goes above catch, because retry should get its attempts in before anything decides to give up and convert the failure into a state. I would predicate retry on the failure being transient, typically IOException, since retrying a parse failure will fail identically every time, and use retryWhen when I want the attempt index so I can back off between tries. The thing to say about retry explicitly is that it re-collects the entire upstream from scratch, so for a cold flow the producer runs again and any side effects in it repeat. onCompletion is different from both because it is not error handling: it runs on every ending, normal completion, failure, and cancellation, with a cause parameter that is null on success. That makes it the right place for symmetric cleanup like hiding a spinner, and the wrong place to try to recover. And the limit worth stating: none of these see an exception thrown inside the collect lambda itself. That needs an ordinary try/catch around the whole collect call, and the fact that catch cannot swallow it is deliberate, because otherwise a bug in your own collector would silently look like an upstream failure.
Explain Flow backpressure to someone coming from RxJava.
What a strong answer covers: The short version is that Flow does not have a backpressure API because it does not need one. In Reactive Streams the subscriber signals demand with request(n), the producer must respect it, and because a producer often cannot, you pick an overflow strategy up front: buffer, drop, latest, error. Flow replaces the whole protocol with suspension. emit is a suspending function, so if the collector is not ready, the producer suspends inside emit until it is. There is no demand signal because the producer physically cannot get ahead. That means a plain chain never drops a value and never grows an unbounded queue, and there is no strategy to configure or get wrong. What you give up is overlap: producer and collector run in lockstep, so the producer waits while the collector works even when they could proceed in parallel. buffer is the explicit opt-out that reintroduces a queue, and its onBufferOverflow parameter is where the Rx-style decision finally appears: SUSPEND keeps the no-loss default, DROP_OLDEST is exactly what conflate is, DROP_LATEST discards the incoming value. The one place the model genuinely leaks is a producer you do not own, a sensor callback or a click listener, which cannot suspend on your behalf. That is what callbackFlow with trySend and an explicit BufferOverflow is for, and it is the only situation where a Flow user has to make the same up-front choice an Rx user always makes.