skip to content

callbackFlow

callbackFlow is the standard adapter for listener APIs: register the listener, push values with trySend, and unregister inside awaitClose. Forgetting awaitClose is the leak interviewers are checking you know about.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What is callbackFlow and when would you reach for it instead of the plain flow { } builder?

level: juniorimportance: must knowfreq 70%

answer

  1. register listener -> trySend -> awaitClose to unregister
  2. bridges callback/listener API into a Flow
  3. trySend is non-suspending, safe from foreign threads
  4. awaitClose is mandatory or it throws
  5. use flow { } when you emit yourself

basics

~10 s

callbackFlow 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 s

callbackFlow { } 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 lines
kotlin
fun connectionState(api: SocketApi): Flow<State> = callbackFlow {
    val cb = SocketApi.Callback { state -> trySend(state) }
    api.addCallback(cb)
    awaitClose { api.removeCallback(cb) }
}

go deeper

for a junior

Knows callbackFlow adapts a listener API and that you must unregister in awaitClose.

for a middle

Explains the channel backing, why flow { } cannot emit from a callback thread, and trySend vs emit.

for a senior

Discusses cold semantics, close vs cancel, and how downstream cancellation drives cleanup deterministically.

for a principal

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

context

open as a page

What is the role of awaitClose { } in callbackFlow, and what happens if you omit it?

level: middleimportance: must knowfreq 65%

basics

~10 s

awaitClose keeps the flow alive until it is cancelled or closed, then runs cleanup like unregistering the listener. If you leave it out, the flow ends immediately and Kotlin throws an error.

open as a page

How do you terminate a callbackFlow from inside, and how does close() differ from cancel()?

level: middleimportance: should knowfreq 45%

basics

~10 s

Call close() when the source is finished normally; collectors stop and cleanup runs. Call cancel() to stop with an error/cancellation. Both resume awaitClose so your listener gets unregistered.

open as a page

Inside a callbackFlow, when do you use trySend versus send, and how do you handle backpressure?

level: seniorimportance: should knowfreq 55%

basics

~10 s

Use trySend in a normal (non-suspending) callback because it never blocks; it just succeeds, fails, or signals closed. Use send only from suspending code that can wait. Control overflow with a buffer.

open as a page

What are the common leak and correctness pitfalls when wrapping a callback API with callbackFlow, and how do you make the adapter robust?

level: principalimportance: nice to knowfreq 35%

basics

~10 s

The big risks are forgetting to unregister the listener, dropping events under load, and emitting after the flow closed. Fix them by always unregistering in awaitClose, choosing a buffer policy, and checking trySend results.

open as a page