Flow: the Basics Interview Questions
KOTLIN › Concurrency
A teammate suggests replacing a suspend function that returns a List<Item> with a Flow<Item> that emits one item at a time. When does that change actually earn its complexity, and when is it just churn?
What a strong answer covers: Flow pays for itself when there's genuinely more than one meaningful value over time, live search results streaming in, a Room query that should update the UI as the database changes, progress updates, not when there's a single request/response pair. For a one-shot fetch, a plain suspend function returning a List is simpler, cheaper, and easier to test; reaching for Flow reflexively for anything async is a tell of someone who's memorized the API rather than understood the shape it's solving for.
You call someFlow.collect { } twice in two different coroutines against the same Flow built with flow { }, and each collector triggers its own separate network request even though you only wanted one. Walk me through why that happens and how you'd fix it.
What a strong answer covers: A cold flow's producer block re-runs independently for every collector, there's no shared or cached execution, so two collect calls really do mean two full runs of the flow { } body including any network call inside it. The fix is to convert the flow to hot with shareIn or stateIn and an appropriate SharingStarted policy, so there's a single upstream collection that all downstream collectors observe instead of each triggering their own.
Why does Kotlin insist that emit() only be called from the flow builder's own coroutine context, and not from, say, a background thread pool's callback? What real bug does that rule actually prevent?
What a strong answer covers: flow { } is not thread-safe by design, this is the context preservation guarantee underpinning exception transparency: emit assumes sequential, single-context execution so ordering and correct exception propagation to the collector can be guaranteed, and emitting from a foreign context throws an IllegalStateException at runtime. The real-world trigger is naively wrapping a callback-based API and calling emit() straight from the callback thread; the actual fix is callbackFlow, which is backed by a channel and explicitly supports sending values from other threads via trySend.