What is callbackFlow and when would you reach for it instead of the plain flow { } builder?
answer
- register listener -> trySend -> awaitClose to unregister
- bridges callback/listener API into a Flow
- trySend is non-suspending, safe from foreign threads
- awaitClose is mandatory or it throws
- use flow { } when you emit yourself
basics
~10 scallbackFlow turns an old-style callback or listener API into a Flow. You use it when values arrive through a callback you register, not from suspending code you call directly.
solid answer
~40 scallbackFlow { } is a Flow builder for bridging callback-based or listener-based APIs into Flow. Inside the block you register a listener and push each event with trySend (it returns a ChannelResult, never suspends to the point of blocking the callback thread), then call awaitClose { } at the end to suspend until the flow is cancelled and unregister the listener there. Unlike flow { }, the producer block runs concurrently with collection and is backed by a channel, so a foreign thread invoking the callback can safely emit. Use plain flow { } when you can simply call emit yourself from suspending/sequential code; use callbackFlow when emissions originate asynchronously from a callback you do not control.
code
kotlin · 5 linesfun connectionState(api: SocketApi): Flow<State> = callbackFlow {
val cb = SocketApi.Callback { state -> trySend(state) }
api.addCallback(cb)
awaitClose { api.removeCallback(cb) }
}go deeper
Knows callbackFlow adapts a listener API and that you must unregister in awaitClose.
Explains the channel backing, why flow { } cannot emit from a callback thread, and trySend vs emit.
Discusses cold semantics, close vs cancel, and how downstream cancellation drives cleanup deterministically.
Frames callbackFlow within a resource-lifecycle/backpressure strategy and standardizes adapter patterns across the codebase.
## The problem callbackFlow solves Many Kotlin/Java/Android APIs are **callback-based**: you register a listener object, and the library calls your method whenever something happens (a location update, a websocket message, a database change). A `Flow` is a cold asynchronous stream of values. `callbackFlow { }` is the **bridge** that adapts one into the other. The plain `flow { }` builder is *sequential and constrained*: emissions must come from the same coroutine via `emit`, and you may not call `emit` from a different thread/coroutine (it throws an `IllegalStateException` for emission context violations). Callbacks fire on **foreign threads** at unpredictable times, so `flow { }` does not fit. ## How callbackFlow works ```kotlin fun locationUpdates(client: LocationClient): Flow<Location> = callbackFlow { val listener = object : LocationListener { override fun onLocation(loc: Location) { trySend(loc) // push into the channel; safe from any thread } } client.register(listener) awaitClose { client.unregister(listener) } // cleanup on cancel/close } ``` - **Backed by a channel** — `callbackFlow` is a `channelFlow` (a `ProducerScope<T>`) whose block can send from any thread. - **`trySend(value)`** — non-suspending offer that returns a `ChannelResult`; the right tool inside a synchronous callback because the callback signature usually cannot suspend. - **`awaitClose { }`** — **mandatory**. It suspends the builder block until the flow is cancelled or `close()`d, then runs its lambda for teardown (unregister the listener). Omitting it throws `IllegalStateException` at runtime because the block would return immediately and the channel would close while the listener is still registered. ## Terminating the flow - **`close(cause?)`** — completes the flow normally (or with a cause); collectors stop. Use it when the source signals "done." - **`cancel(cause)`** — cancels with a `CancellationException`. - **Downstream cancellation** — if the collector's scope is cancelled, `awaitClose` resumes and your cleanup runs. ## Key APIs/keywords `callbackFlow`, `ProducerScope`, `trySend`, `send` (suspending), `awaitClose`, `close`, `cancel`, `ChannelResult`, `Flow`.
- Why can't you just use flow { } and call emit from inside the callback?emit must be called from the flow's own coroutine context; calling it from a foreign callback thread violates the emission-context check and throws IllegalStateException. callbackFlow is channel-backed so cross-thread sends are allowed.
- Is callbackFlow cold or hot?Cold: the producer block (listener registration) runs fresh on each collection and stops when collection stops.
callbackFlow is an answering machine: it records messages left by callers (callbacks) whenever they call, and you pick them up later by collecting.
saying these in an interview costs you the question
- Says callbacks can emit directly with emit() in flow { }
- Forgets awaitClose entirely
- Thinks callbackFlow is hot/shared by default
- Confuses it with StateFlow/SharedFlow
- Says trySend suspends