Phase 6 · Coroutines & AsyncModule 33~40 min read

Flows

Handle streams of asynchronous data with cold Flows and StateFlow/SharedFlow.

What you'll learn

A suspend function returns a single value. A Flow returns many values over time — asynchronously, one at a time. Flows are Kotlin's reactive streams: think of them as a suspend-friendly, coroutine-native sequence for events, updates, and data that arrives bit by bit.

By the end you'll be able to:

  • Explain what a cold flow is
  • Build flows with flow{}, flowOf, and asFlow
  • Transform and collect flows with operators
  • Use StateFlow and SharedFlow for hot streams

Cold streams

A flow is cold: the code inside flow { ... } doesn't run until a terminal operator like collect subscribes. Each new collector gets its own fresh run. Values are pushed with emit and received in collect:

Cold.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

// A Flow is a COLD stream: nothing runs until someone collects it.
fun countdown(): Flow<Int> = flow {
    for (i in 3 downTo 1) {
        delay(300L)
        emit(i)                     // push a value downstream
    }
    emit(0)
}

fun main() = runBlocking {
    countdown().collect { value ->  // collect() starts the flow
        println("tick $value")
    }
}

Key idea

A cold flow is like a recipe, not a meal — describing it does nothing; collecting it cooks it. Because the body is suspending, you can call delay, hit the network, or read a database between emissions, all without blocking a thread.

Building flows

There are three everyday ways to create a flow:

BuilderUse it when
flow { emit(x) }you compute/emit values yourself, possibly suspending between them
flowOf(a, b, c)you already have a fixed set of values
list.asFlow()you want to turn an existing collection or range into a flow

Operators & collecting

Flows carry the same declarative operators you learned for collections — map, filter, take, onEach — but they're intermediate: lazy, and only run when a terminal operator (collect, toList, first) drives the stream:

Operators.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    (1..5).asFlow()
        .filter { it % 2 == 1 }     // keep odd numbers
        .map { it * it }            // square them
        .collect { println(it) }    // terminal operator
}

Note

Operators run in the collector's coroutine, so back-pressure is automatic: a slow collector simply slows the whole chain — the producer waits rather than overflowing a buffer. To decouple them, insert buffer() or flowOn(dispatcher) to move the upstream work elsewhere.

StateFlow & SharedFlow

Sometimes you want a hot stream that exists independently of collectors. StateFlow always holds a single current value (perfect for UI state), and SharedFlow broadcasts events to multiple collectors. New subscribers to a StateFlow immediately receive the latest value:

State.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val state = MutableStateFlow(0) // HOT: always holds a current value

    val watcher = launch {
        state.collect { println("state = $it") }
    }
    delay(50L); state.value = 1
    delay(50L); state.value = 2
    delay(50L); watcher.cancel()    // a hot flow is collected until cancelled
}

Tip

StateFlow is the modern replacement for LiveData in Android — it drives Jetpack Compose UIs beautifully. Expose a read-only StateFlow publicly and keep the MutableStateFlow private.

Combining flows

Flows compose. zip pairs values one-for-one, combine re-emits whenevereither source changes, and flatMapConcat/flatMapMerge flatten a flow of flows:

Zip.kt
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun main() = runBlocking {
    val nums = flowOf(1, 2, 3)
    val letters = flowOf("A", "B", "C")
    nums.zip(letters) { n, l -> "$n$l" }   // pair them up
        .collect { println(it) }
}

Recap & quick check

Key takeaways

  • A Flow emits many values over time asynchronously — a coroutine-native reactive stream.
  • Cold flows don't run until collected, and each collector triggers a fresh run.
  • Build flows with flow{}, flowOf(...), or collection.asFlow(); push values with emit.
  • Intermediate operators (map, filter) are lazy; terminal operators (collect, toList) drive the stream.
  • StateFlow holds a current value and SharedFlow broadcasts events — both are hot, ideal for UI state.

Quick check

1. What does it mean that a Flow is 'cold'?

2. How do you emit a value from inside flow { }?

3. Which is a terminal operator?

4. What is StateFlow best suited for?

5. How does a flow handle a slow collector by default?

Flows model streams elegantly. For lower-level, imperative communication between coroutines — queues, producers, and consumers — we turn next to channels and shared state.