skip to content

What is channelFlow { } and how does it differ from the plain flow { } builder?

level: juniorimportance: must knowfreq 60%

answer

  1. Cold Flow backed by a Channel
  2. send() not emit(); thread-safe
  3. Block is a CoroutineScope — can launch
  4. flow{} = single-context emit only
  5. Channel auto-closes at block end

basics

~10 s

channelFlow builds a cold Flow but lets you emit values from several coroutines at once using send(). The plain flow builder only lets you emit from one place, sequentially.

solid answer

~40 s

channelFlow { } is a cold Flow builder backed by a Channel. Inside the block you call send() (a suspend function) instead of emit(), and the block runs inside a CoroutineScope, so you can launch child coroutines that all send concurrently. flow { } in contrast restricts you to a single emit() call chain that must run in one coroutine/context — emitting from another coroutine throws IllegalStateException because of flow's context-preservation rule. channelFlow relaxes that: send() is thread-safe and may be called from any child coroutine. The builder is still cold (nothing runs until collected) and the channel is closed automatically when the block completes. Use channelFlow when you need concurrency or multiple producers; use flow { } for simple sequential emission.

code

kotlin · 13 lines
kotlin
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*

fun concurrent(): Flow<Int> = channelFlow {
    launch { send(1) }
    launch { send(2) }
    send(3)
}

// This would FAIL with the plain flow builder:
// fun broken(): Flow<Int> = flow {
//     launch { emit(1) } // IllegalStateException: Flow invariant is violated
// }

go deeper

for a junior

States that channelFlow is a cold Flow builder using send() and allows emitting from multiple coroutines, unlike flow { }.

for a middle

Explains the context-preservation rule that flow { } enforces and how the backing channel + thread-safe send() lift it.

for a senior

Discusses backpressure via send() suspension, default rendezvous capacity, and when the channel allocation cost is/ isn't worth it versus flow { }.

for a principal

Frames channelFlow's role in a fan-in/producer-consumer design and the trade-offs of channel overhead and ordering guarantees at scale.

## What channelFlow is `channelFlow { }` is a **cold Flow builder** from `kotlinx.coroutines.flow`. "Cold" means the producer block does not run until a collector calls a terminal operator (like `collect`); each collection starts a fresh execution. Unlike the plain `flow { }` builder, the lambda you pass to `channelFlow` is a `suspend ProducerScope<T>.() -> Unit`. `ProducerScope` is both a `CoroutineScope` and a `SendChannel<T>`. That gives you two powers: - You can **`launch` child coroutines** inside the block. - You emit with **`send(value)`** (a `suspend` function) or `trySend(value)`, instead of `emit()`. ## Why it differs from flow { } The plain `flow { }` builder enforces **context preservation**: `emit()` must be called from the same coroutine that runs the block. Emitting from a different coroutine/thread throws `IllegalStateException: Flow invariant is violated`. So `flow { }` is strictly sequential and single-producer. `channelFlow` removes that restriction by routing values through a backing `Channel`. `send()` is **thread-safe**, so multiple child coroutines can produce values concurrently and they are funneled through the channel to the single collector. ```kotlin fun numbers(): Flow<Int> = channelFlow { launch { send(1) } // child coroutine 1 launch { send(2) } // child coroutine 2 send(3) // the builder coroutine itself } // channel auto-closes when block (and children) finish ``` ## Key mechanics - The backing channel is **closed automatically** when the block completes; you don't call `close()` yourself (and shouldn't). - `send()` **suspends** when the channel/buffer is full, applying backpressure to producers. - By default the buffer is **rendezvous** (capacity 0) unless you add a `.buffer()` operator downstream. - `awaitClose { }` is **not** needed here (that's for `callbackFlow` with external callbacks); `channelFlow` finishes when the block's coroutines finish. ## When to use it Reach for `channelFlow` when you need **concurrent emission**, **multiple producers**, or fan-in. For simple sequential streams, prefer `flow { }` — it's lighter and doesn't allocate a channel.

  • Do you ever need to call close() on the channel inside channelFlow?
    No. The channel is closed automatically once the builder block and all its child coroutines complete. Calling close() yourself is unnecessary and can truncate emissions.
  • Is channelFlow hot or cold?
    Cold. Nothing runs until a collector subscribes, and each collection re-runs the block independently. Hotness would come from SharedFlow/StateFlow or sharing operators, not from channelFlow itself.

flow { } is one cashier ringing up items in order; channelFlow is several cashiers feeding one conveyor belt (the channel) that the collector reads.

saying these in an interview costs you the question

  • Saying channelFlow is hot (it is cold)
  • Calling emit() inside channelFlow (it's send())
  • Thinking flow { } can emit from launched coroutines
  • Manually closing the channel inside the block
  • Confusing channelFlow with callbackFlow/awaitClose

context