Flow: the Basics Explained

KOTLIN › Concurrency

You already know a suspend fun returns a single value, once:

suspend fun loadUser(): User   // one User, then it's done

But lots of things aren't one value, they're a **stream of values that arrive over time**: location updates as you move, rows coming back from a database, search results as the user types. A single return can't model that.

A **Flow** is Kotlin's type for exactly this: an asynchronous stream that produces zero, one, or many values over time. The clearest mental model is *a List you receive one item at a time* rather than all at once, or *a Sequence that's allowed to pause and wait* between items.

val numbers: Flow<Int> = flow {
    emit(1)   // hand out a value
    emit(2)
    emit(3)
}

numbers.collect { n -> println(n) }   // 1, 2, 3

emit sends a value out; collect receives each one. The rest of this lesson takes each half apart properly. The one idea to hold onto: a Flow is a **stream**, not a single result.

Every Flow has two halves that work as a pair.

The **producer** is the flow { } block. Inside it you call **emit(value)** to hand out values, one at a time, in order:

val ticker = flow {
    emit("first")
    delay(1000)        // wait a second
    emit("second")
}

The **collector** is whoever calls **collect { }**. Its lambda runs once for every value the producer emits:

ticker.collect { value ->
    println(value)     // "first", then a second later, "second"
}

Here's the key: producer and collector are wired together. Each emit sends one value straight to the collector's block, waits for it to finish, and only then moves on to the next line. Nothing is buffered or fired-and-forgotten by default, it's a controlled, one-at-a-time handoff.

The reason Flow is so pleasant to write: everything inside it is **suspending**, so you write straight-line code instead of callbacks.

Inside a flow { } you can call any suspend function, a network request, a database read, delay, right before you emit the result:

val prices = flow {
    while (true) {
        val price = api.fetchPrice()   // suspend network call
        emit(price)
        delay(5000)                    // poll every 5s
    }
}

No callback interfaces, no listener registration, no manual threading. Compare that to the old world of setOnPriceChangedListener { ... } with nested callbacks: Flow turns it into a simple loop. And because collect is itself a suspend function, the whole thing is **cancellable**, if the coroutine collecting this flow is cancelled, the loop stops cleanly.

Creating a Flow runs **nothing**. val f = flow { ... } just builds a description of the stream, like writing down a recipe without cooking it. The producer block doesn't execute until a **terminal operator** starts it, and the most common one is collect.

val f = flow { println("running!"); emit(1) }
// nothing has printed yet

f.collect { }   // NOW "running!" prints and 1 is emitted

Because collect is a suspend function, it has to run inside a coroutine, you'll almost always see it in a launch { }, a viewModelScope, or another suspend function:

viewModelScope.launch {
    f.collect { value -> updateUi(value) }
}

Hold onto that: nothing runs until something actually drives the flow. The next part covers exactly which calls count as driving it, and which only describe it.

Once you have a flow, you'll usually shape it before collecting, filtering values out or transforming them. That's what operators like map, filter, and onEach are for. But those are **intermediate operators**, and they're just as lazy as the flow itself. Calling .map { } doesn't loop over anything or run any code, it just wraps the flow in a new description: when someone eventually collects, transform each value like this.

val doubled = flow {
    emit(1)
    emit(2)
    emit(3)
}.map { it * 2 }   // just wraps, runs nothing yet

doubled.collect { println(it) }   // NOW it runs: 2, 4, 6

Nothing in that chain actually runs until a **terminal operator**, collect, toList, or first, drives it from the start. That's the split worth remembering: intermediate operators (map, filter, onEach) describe a transformation and stay lazy; terminal operators (collect, toList, first) are what actually starts the producer and pulls values through the whole chain.

One more foundational fact, and it's the bridge to the next lesson. Because a Flow is just a recipe that runs when collected, **each collector triggers its own independent run** of the producer:

val f = flow {
    println("producer starting")
    emit(1)
}

launch { f.collect { } }   // "producer starting" prints
launch { f.collect { } }   // "producer starting" prints AGAIN

Two collectors, two separate runs, nothing shared between them by default. This property has a name: a Flow like this is **cold** (it comes alive only when collected, and re-runs per collector), as opposed to a **hot** stream that's always running and shared. You don't need the hot side yet, just hold onto "cold = runs on demand, once per collector."

That's the whole foundation. A Flow is a stream you build with flow { emit(...) }, shape with lazy intermediate operators like map and filter, and actually run with a terminal operator like collect { } inside a coroutine. It does nothing until collected, and it's cold. If an interviewer asks what a Flow is, that's your answer: an asynchronous stream, cold and lazy by default, one producer run per collector. The **Flow & Operators** lesson picks up right here, everything you can do to that stream: transform it, switch threads, handle errors, combine two of them.

Back to Flow: the Basics