Flow and StateFlow cheat sheet

The 14 Flow and StateFlow interview answers in 30 seconds each, plus quick facts. Printable, free.

One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions

Rules of thumb

  • State is StateFlow; streams to several listeners are SharedFlow.
  • Collect in the UI with repeatOnLifecycle or collectAsStateWithLifecycle.
  • stateIn with WhileSubscribed(5_000) for screen state.
  • catch handles upstream only, and the flow completes after it.
  • flatMapLatest cancels stale requests.

Cold vs hot flows

A cold Flow runs its producer code again for every collector, and only while someone collects. Nothing happens until collect is called. A hot flow such as StateFlow or SharedFlow exists independently of collectors: values are emitted whether or not anyone listens, and all collectors share the same stream.

StateFlow vs SharedFlow

StateFlow holds exactly one current value, always has an initial value, replays it to new collectors and skips values equal to the last one. It models state, like screen UI state. SharedFlow has no current value, can replay zero or more items and doesn't de-duplicate, so it suits broadcasts of events to several listeners.

stateIn and WhileSubscribed(5_000)

stateIn turns a cold flow, such as a Room query, into a StateFlow the UI can collect. WhileSubscribed(5_000) keeps the upstream running while someone collects and for 5 seconds after the last collector leaves. That covers a rotation, where the UI stops and restarts collecting within a second, without restarting the query, while still stopping it when the app goes to the background.

@Entity
data class Note(@PrimaryKey val id: Long, val text: String)

@Dao
interface NoteDao {
    @Query("SELECT * FROM Note ORDER BY id DESC")
    fun observeAll(): Flow<List<Note>>
}

One-off events from a ViewModel

The current Android guidance is to model events as state: put the message in UI state, show it, then tell the ViewModel it was handled so it clears it. That survives rotation and can't be lost. If you use an event stream, a buffered Channel received with receiveAsFlow delivers each event to one collector and holds events while nobody listens.

data class EditorState(val text: String = "", val message: String? = null)

class EditorStateViewModel : ViewModel() {
    private val _state = MutableStateFlow(EditorState())
    val state = _state.asStateFlow()

    fun save() = viewModelScope.launch {
        saveToDisk(_state.value.text)
        _state.update { it.copy(message = "Saved") }
    }

    fun onMessageShown() = _state.update { it.copy(message = null) }
}

map, filter and transform

They are intermediate operators: each returns a new cold flow and does nothing until a terminal operator such as collect, first or toList runs. map changes each value, filter drops some, onEach runs a side effect and passes the value through, and transform can emit any number of values per input.

Search-as-you-type with Flow

Keep the query in a StateFlow, then debounce(300) to wait until typing pauses, distinctUntilChanged() to skip repeats, and flatMapLatest { api.search(it) } so a new query cancels the request for the previous one. Only the latest query's result can reach the UI, so a slow old response can't overwrite a newer one.

combine vs zip

combine emits whenever either flow emits, using the latest value from each, once both have emitted at least once. zip pairs values one-to-one: the first with the first, the second with the second, and waits for both. Use combine for UI state built from several sources; zip is rarely right for UI.

flowOn and context preservation

Use flowOn(Dispatchers.IO): it changes the dispatcher for the operators above it, while the collector keeps its own context. A flow must emit from the same coroutine context it was collected in, so calling emit inside withContext breaks that rule and throws IllegalStateException at runtime.

fun readLines(file: File): Flow<String> = flow {
    file.bufferedReader().useLines { lines ->
        for (line in lines) emit(line)
    }
}.flowOn(Dispatchers.IO)

Handling errors in a Flow

catch { } handles exceptions from the operators above it, not from below or from the collector. Inside it you can emit a fallback value or rethrow. After catch, the flow completes, so for a long-lived stream put recovery inside an inner flow or use retry/retryWhen before catch to restart the upstream.

Wrapping a listener with callbackFlow

Use callbackFlow. Register the listener inside it, call trySend from the callback, and end with awaitClose { unregister() }. awaitClose keeps the flow open until the collector cancels, then removes the listener. Forgetting it is the classic bug: the builder throws, or the listener leaks.

fun ConnectivityManager.onlineFlow(): Flow<Boolean> = callbackFlow {
    val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) { trySend(true) }
        override fun onLost(network: Network) { trySend(false) }
    }
    registerDefaultNetworkCallback(callback)
    awaitClose { unregisterNetworkCallback(callback) }
}.distinctUntilChanged()

buffer, conflate and collectLatest

By default a flow is sequential: the producer waits while the collector works. buffer() lets the producer run ahead into a queue. conflate() keeps only the latest value, dropping ones the collector missed. collectLatest cancels the collector's work for an old value when a new one arrives. Pick based on whether every value matters.

Why didn't my StateFlow update?

StateFlow only emits when the new value is not equal to the old one. If you mutate a MutableList in place and assign the same list back, the old and new values are the same object, so nothing is emitted. Hold immutable data and replace it with a copy: state.update { it.copy(items = it.items + item) }.

LiveData vs StateFlow

New Kotlin apps generally use StateFlow. It is part of coroutines, works in plain Kotlin modules and Kotlin Multiplatform, has a rich operator set, and always has an initial value. LiveData is lifecycle-aware on its own, but StateFlow gets the same safety from repeatOnLifecycle or collectAsStateWithLifecycle. LiveData still works fine in existing Java or View code.

Channels

A Channel is a hot, concurrent queue between coroutines: send suspends when the buffer is full and receive suspends when it's empty. Each element is received by exactly one receiver. A Flow is a cold description of values. Channels are for handing work between coroutines; flows are for exposing streams.

Quick facts

MutableStateFlow.update
update { } applies a change with compare-and-set and retries, so concurrent updates are never lost.
shareIn replay
shareIn(scope, started, replay = 1) is like stateIn without the initial value and equality check.
SharingStarted.WhileSubscribed
WhileSubscribed(stopTimeoutMillis) keeps the upstream running that long after the last collector leaves.
debounce
debounce(300) emits a value only after 300 ms pass with no newer value.
first()
first() collects until the first value, returns it and cancels the upstream.