Skip to content
AgileLogics
6 min read

StateFlow, SharedFlow, and Channels

Prerequisites: Flow: asynchronous streams

Regular flows are cold and start fresh for each collector. Apps also need hot streams that exist independently of collectors — like "the current state of this screen".

StateFlow — observable state

A StateFlow always holds one current value. New collectors receive the latest value immediately, then every change:

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

class CounterModel {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() {
        _count.update { it + 1 }
    }
}

fun main() = runBlocking {
    val model = CounterModel()
    val job = launch {
        model.count.collect { println("Count: $it") }
    }
    delay(50)
    model.increment()
    delay(50)
    model.increment()
    delay(50)
    job.cancel()
    println("Final: ${model.count.value}")
}
Count: 0
Count: 1
Count: 2
Final: 2
  • The class keeps a private MutableStateFlow and exposes a read-only StateFlow — only the model can change the state.
  • update { } changes the value safely, even from several threads.
  • .value reads the current value directly.

This is exactly how Android ViewModels expose UI state: the screen collects the StateFlow and redraws whenever it changes.

SharedFlow — events for many listeners

A SharedFlow broadcasts values to all current collectors, without holding a "current" value (unless configured to replay some). Use it for events such as "show a message" or "navigate to checkout".

Channels — hand values between coroutines

A Channel is a queue between coroutines: one sends, another receives, and each value is delivered to exactly one receiver:

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val jobs = Channel<Int>()

    launch {
        for (n in 1..3) jobs.send(n)
        jobs.close()
    }

    for (job in jobs) {
        println("Processing job $job")
    }
}
Processing job 1
Processing job 2
Processing job 3

for (job in jobs) receives until the channel is closed. Several coroutines reading from one channel form a worker pool.

Choosing

Need Use
Current state that UI observes StateFlow
Broadcast events to several listeners SharedFlow
A work queue between coroutines Channel
A sequence of values computed on demand Flow