Flow & Channels
Reviewed & published by Brayan K
A Flow is a cold asynchronous stream that emits values over time and only runs when collected, while a Channel is a hot queue passing values between coroutines.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll learn the flow { emit() } builder, why flows are cold, collecting values, operators like map/filter, how flows differ from sequences, and the basics of Channel.
What You'll Learn in This Lesson
- Building a stream with flow { emit() }
- Why a Flow is cold and lazy
- Collecting values with collect
- Operators: map and filter on a flow
- How flows differ from sequences
- Channel basics: send, receive, close
1️⃣ Cold Flows: flow { emit() }
A Flow is an asynchronous stream. You build one with the flow { } builder and push values with emit(). It's cold: the builder doesn't run until a terminal operator like collect consumes it. Each collector restarts it from the beginning.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// A Flow is a COLD asynchronous stream: the flow { } block doesn't run
// until something calls collect. Then it emits values one at a time.
fun numbers(): Flow<Int> = flow {
for (i in 1..3) {
emit(i) // send a value to the collector
}
}
fun main() = runBlocking {
println("before collect") // flow has NOT run yet
numbers().collect { value ->
println("got $value")
}
println("done")
}Notice "before collect" prints first — proof the flow body waited until collect pulled values through it.
2️⃣ Flow Operators: map & filter
Flows share the familiar operators — map, filter, take — but they run lazily and may suspend. Each returns a new flow; nothing happens until a terminal operator like collect runs. flowOf(...) is a quick way to make a flow from fixed values.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// Flow has the same operators as collections — map, filter — but they
// run lazily and can SUSPEND. They return a new flow; collect triggers it.
fun main() = runBlocking {
flowOf(1, 2, 3, 4, 5)
.filter { it % 2 == 1 } // keep odd numbers: 1, 3, 5
.map { it * it } // square them: 1, 9, 25
.collect { println(it) } // terminal: pulls values through
}A single collector processes values in emission order, so 1, 9, 25 always print in exactly that sequence — the same mental model as a sequence, but able to suspend.
3️⃣ Channels: Hot Queues Between Coroutines
Where a flow is cold, a Channel is hot: it's a coroutine-safe queue. One coroutine sends values, another receives them, and each value goes to exactly one consumer. Closing the channel lets a for loop drain it cleanly.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
// A Channel is a HOT stream — like a queue between coroutines. A producer
// sends; a consumer receives. We close the channel, then drain it in order.
fun main() = runBlocking {
val channel = Channel<Int>()
// Producer coroutine sends three values then closes:
launch {
for (x in 1..3) channel.send(x)
channel.close()
}
// Consume every value in send order until the channel is closed:
for (y in channel) {
println("received $y")
}
println("channel done")
}The consumer's for loop receives values in send order until the producer calls close(), so the output is fully ordered.
Your turn. Fill in the ___, then run and compare.
// 🎯 YOUR TURN — replace the ___ then run main()
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
flowOf(10, 20, 30)
.map { it + 1 }
// Use the terminal operator that runs the flow and gives each value:
.___ { println(it) } // 👉 put "collect" in the blank
// ✅ Expected:
// 11
// 21
// 31
}Mini-Challenge: Build & Transform a Flow
Create a flow, filter and map it, then collect — all with deterministic, ordered output.
// 🎯 MINI-CHALLENGE: build and transform a flow.
// Save as Stream.kt and run main().
//
// 1. Write fun source(): Flow<Int> = flow { emit 1, 2, 3, 4 in a loop }.
// 2. In runBlocking, take source() and:
// - keep only EVEN values
// - multiply each by 10
// - collect and print each result
// (Collect runs them in emission order, so output is deterministic.)
//
// ✅ Expected output:
// 20
// 40
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
// your code here
}Common Errors (and the fix)
- ❌ Flow body never runs — You built a flow but never collected it. ✅ Add a terminal operator like collect { }, toList(), or first().
- ❌ "emit can only be called within builder" — You called emit outside a flow { } block. ✅ Emit only inside the builder.
- ❌ Collecting outside a coroutine — collect is suspending. ✅ Call it inside runBlocking or another coroutine.
- ❌ Channel for loop hangs — The producer never called close(). ✅ Close the channel when done so the consumer loop ends.
Pro Tips
- 💡 Think "sequence that can suspend": flows are the async cousin of sequences.
- 💡 Cold means repeatable: collect the same flow twice and it runs twice from the start.
- 💡 Prefer Flow over Channel for most streaming; reach for a Channel only for true coroutine-to-coroutine handoff.
📋 Quick Reference — Flow & Channels
| Concept | Kotlin Syntax |
|---|---|
| Build a flow | flow { emit(x) } |
| From fixed values | flowOf(1, 2, 3) |
| Transform | .map { } / .filter { } |
| Consume | .collect { ... } |
| Make a channel | val ch = Channel<Int>() |
| Send / close | ch.send(x); ch.close() |
🎉 Lesson Complete!
- ✅ A Flow is a cold async stream built with flow { emit() }
- ✅ Nothing runs until a terminal operator like collect
- ✅ Operators (map, filter) are lazy and can suspend
- ✅ Flows are like sequences but able to suspend
- ✅ A Channel is a hot queue handing values between coroutines
Practice quiz
What does it mean that a Flow is 'cold'?
- It runs on a background thread only
- The builder block does not run until a terminal operator collects it
- It can never be collected twice
- It blocks the main thread
Answer: The builder block does not run until a terminal operator collects it. A cold flow does not run until a terminal operator like collect starts consuming it.
Which builder pushes values into a flow?
- send()
- yield()
- push()
- emit()
Answer: emit(). Inside flow { }, you push values with emit().
Which is a terminal operator that starts a flow?
- collect
- map
- filter
- flowOf
Answer: collect. collect is the terminal operator that actually runs the flow and receives each value.
How do intermediate operators like map and filter behave on a flow?
- They run eagerly and block
- They run lazily and return a new flow; nothing happens until collected
- They consume the flow immediately
- They cannot suspend
Answer: They run lazily and return a new flow; nothing happens until collected. Intermediate operators are lazy, return a new flow, and may suspend; collect triggers them.
How does a Flow differ from a Sequence?
- A Flow cannot have operators
- A Sequence is asynchronous
- A Flow is built for coroutines and can suspend; a Sequence is synchronous and cannot
- They are identical
Answer: A Flow is built for coroutines and can suspend; a Sequence is synchronous and cannot. A Flow can suspend at every step; a Sequence is synchronous and cannot suspend.
Why is the output of a single collector deterministic in order?
- Because flows sort their output
- Because one collector consumes emissions sequentially in production order
- Because collect runs in parallel
- Because emit randomizes order
Answer: Because one collector consumes emissions sequentially in production order. A single collector receives emitted values one at a time in the order produced.
How is a Channel best described?
- A cold, repeatable stream
- A synchronous list
- An extension property
- A hot, coroutine-safe queue between coroutines
Answer: A hot, coroutine-safe queue between coroutines. A Channel is a hot stream: a coroutine-safe queue passing values between coroutines.
In a Channel, each sent value is received by...
- Exactly one consumer
- Every consumer simultaneously
- No consumer
- Only the producer
Answer: Exactly one consumer. Each Channel value is received by exactly one consumer.
Why must a producer call channel.close()?
- To free CPU cores
- So a for loop over the channel can end cleanly instead of hanging
- To restart the channel
- To make it cold
Answer: So a for loop over the channel can end cleanly instead of hanging. Closing lets the consumer's for loop drain remaining values and then finish.
Where must collect be called from?
- From any ordinary function
- From the top level only
- From inside a coroutine, such as runBlocking
- From a constructor
Answer: From inside a coroutine, such as runBlocking. collect is suspending, so it must be called inside a coroutine like runBlocking.
Continue this course
- Previous: Coroutine Scope, Context & Dispatchers
- Next: Checkpoint: Functions & OOP — Combine interfaces, objects and a function pipeline in a build — then a quiz
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What is a Flow and how is it 'cold'?
A Flow is an asynchronous stream of values produced over time, built with the flow { } builder where you emit each value. It is cold, meaning the builder block does not run until a terminal operator like collect starts consuming it — and it runs again from scratch for each new collector. This is the opposite of a hot stream that produces values whether or not anyone is listening. Coldness makes flows lazy and repeatable, much like sequences but able to suspend.
How does a Flow differ from a Sequence?
Both are lazy and process elements one at a time through a chain of operators, but a Sequence is synchronous and cannot suspend, while a Flow is built for coroutines and can suspend at every step — its emit and its operators may await network calls, timers, or other suspending work without blocking a thread. Use a Sequence for in-memory, CPU-bound lazy pipelines, and a Flow when the values arrive asynchronously over time or each step needs to suspend.
What does collect do, and why is ordering deterministic?
collect is the terminal operator that starts the flow and receives each emitted value in turn, running your lambda for every one. Because a single collector consumes emissions sequentially in the order they are produced, a flow like flowOf(1,2,3).map { }.collect { } always prints its results in that same order. The intermediate operators (map, filter, take) only describe the pipeline; collect is what actually pulls values through it.
What is a Channel and how is it different from a Flow?
A Channel is a hot communication primitive — essentially a coroutine-safe queue where one or more producers send values and one or more consumers receive them. Unlike a cold Flow, a Channel exists and conveys values independently of collectors, and each value is received by exactly one consumer. You typically close a channel when the producer is finished, and a for loop over the channel receives every value in send order until it is closed, which is why the example drains it deterministically.