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

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)

Pro Tips

📋 Quick Reference — Flow & Channels

ConceptKotlin Syntax
Build a flowflow { emit(x) }
From fixed valuesflowOf(1, 2, 3)
Transform.map { } / .filter { }
Consume.collect { ... }
Make a channelval ch = Channel<Int>()
Send / closech.send(x); ch.close()

🎉 Lesson Complete!

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

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.