Wrapping a listener with callbackFlow

Flow and StateFlow Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Spot the bug

Collecting this flow fails at once with IllegalStateException, and the listener is never unregistered. Which line is missing a step?

  1. fun ConnectivityManager.isOnline(): Flow<Boolean> = callbackFlow {
  2. val callback = object : ConnectivityManager.NetworkCallback() {
  3. override fun onAvailable(network: Network) { trySend(true) }
  4. override fun onLost(network: Network) { trySend(false) }
  5. }
  6. registerDefaultNetworkCallback(callback)
  7. }
Hint 1 of 1

What should happen when the collector cancels?

Choose an answer, then open Show answer below to check it.

Show answer
  • Answer: line 6 registerDefaultNetworkCallback(callback)

Nothing keeps the flow open or unregisters the callback: callbackFlow throws IllegalStateException when its block returns without awaitClose, and the registered callback is never removed. After registering, add awaitClose { unregisterNetworkCallback(callback) }.

30-second answer

Use callbackFlow. Register the listener inside it, call trySend from the callback, and end with awaitClose { unregister() }. awaitClose keeps the flow open until the collector cancels, then removes the listener. Forgetting it is the classic bug: the builder throws, or the listener leaks.

Practise saying it · 35 s. No recording, just a timer

Explain how to wrap a listener in a Flow.

Say your answer out loud, then check which of these points you covered:

  • callbackFlow
  • trySend from the callback
  • awaitClose to unregister
  • suspendCancellableCoroutine for one-shot

Deep dive

Use trySend, not send, from a non-suspending callback; handle a full buffer with buffer or conflate downstream.

Call close(exception) from an error callback to fail the flow.

The flow is cold: each collector registers its own listener. Share it with shareIn if several screens need it.

For one-shot callbacks use suspendCancellableCoroutine instead.

Connectivity as a Flow Compile-checked · Kotlin 2.4.21
fun ConnectivityManager.onlineFlow(): Flow<Boolean> = callbackFlow {
    val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) { trySend(true) }
        override fun onLost(network: Network) { trySend(false) }
    }
    registerDefaultNetworkCallback(callback)
    awaitClose { unregisterNetworkCallback(callback) }
}.distinctUntilChanged()

Common mistakes

  • Leaving out awaitClose, so the listener is never removed.
  • Calling send from a callback that isn't a coroutine.
  • Using callbackFlow for a callback that fires only once.

Senior take & follow-ups

Mention the full lifecycle: register, trySend, awaitClose to unregister. That's exactly what interviewers check.

Follow-up questions

Think of your answer first, then open the hint.

  1. What happens if the collector is cancelled?

    Show hint

    awaitClose's block runs and the listener is unregistered.

  2. How would two screens share one listener?

    Show hint

    shareIn in an app scope with WhileSubscribed.

Compare on iOS: Bridging callbacks with AsyncStream AsyncStream with onTermination is Swift's version of callbackFlow with awaitClose.

Suggest a fix if something here is wrong or out of date.