Kotlin / Concurrency, interoperability, and integrated design / Lesson 15
Distinguish cold streams from shared current state
Use Flow transformations and collection while respecting backpressure, context, cancellation, and StateFlow conflation.
On this page
Lesson goals
- Explain when a cold flow starts work and why StateFlow is different.
- Choose buffering or state conflation only when skipped work preserves meaning.
A Flow describes values produced over time
Flow represents asynchronous values using suspending collection. A typical flow builder is cold: its producer block runs when collected, and a separate collection can run it again. Merely assigning a flow does not start its work. This differs from a List already holding values and from a hot shared source whose lifetime exists independently of collectors. State whether repeated collection repeats an expensive request or external action. A stream type does not by itself promise durability, replay, exactly-once effects, or one producer shared across all subscribers.
Keep a simple sequential stream easy to inspect
Intermediate operations transform a flow description, while a terminal collector requests values. The sample uses flowOf, map, filter, and collect without timing or dispatcher changes, so the printed order follows the input order. It is a complete source file requiring a compatible kotlinx.coroutines dependency, but it was not compiled here. The arithmetic is deliberately small: the important lesson is that a transformation returns a stream of results rather than a mutable list that has already completed all its work before collection begins.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
val values = flowOf(1, 2, 3)
.map { it * 10 }
.filter { it >= 20 }
values.collect { println(it) }
}
20
30
Expected output is 20 then 30. No buffer or concurrency operator changes this simple pipeline’s ordering. No coroutine runtime or compiler execution is claimed.
Buffering and dropping values are semantic choices
A straightforward flow can let a slow collector suspend upstream production. buffer introduces a boundary with buffering behavior, conflate can skip intermediate values in favor of newer ones, and collectLatest cancels the previous collector action when a new value arrives. These choices are not interchangeable speed switches. A progress indicator may tolerate skipped intermediate percentages, while a sequence of financial or reservation events may not. Define capacity and overflow policy where the API supports them, and ensure cancelled collector work does not leave an unaccounted irreversible side effect.
StateFlow publishes current state rather than every event
StateFlow is hot, always has a current value, and uses equality-based conflation. A slow subscriber may skip intermediate updates and see the latest state, so it is unsuitable as a durable record of every reservation event. Expose a read-only state view while keeping mutation inside the owning model. Prefer immutable data values and new copies for updates; mutating a list inside the same state object can fail to produce the expected distinct update. The example reads the final value directly and avoids an indefinitely collecting program.
import kotlinx.coroutines.flow.*
data class ScreenState(val selected: Int)
fun main() {
val mutable = MutableStateFlow(ScreenState(0))
val state: StateFlow<ScreenState> = mutable.asStateFlow()
mutable.update { current -> current.copy(selected = current.selected + 1) }
println(state.value.selected)
}
1
Expected output is 1. update atomically transforms the current state, and the pure lambda may be evaluated more than once under contention. Keep side effects out of it. This source requires kotlinx.coroutines and is unexecuted.
Preserve context and failure boundaries through the stream
Flow context rules prevent arbitrary emission from an unrelated context inside a standard flow builder. flowOn changes upstream execution context, not the downstream collector’s context. A catch operator handles upstream failures according to its placement and does not universally catch exceptions thrown by downstream collection. Preserve cancellation rather than converting it to a default value. Hot-state lifetime, sharing policies, and test scheduling need their own design. This lesson claims reasoned outputs only; no stream timing, subscriber race, dispatcher behavior, or runtime library compatibility was executed in the authoring environment.
Common errors and fixes
Creating a cold flow is expected to start the request.
- Why it happens
- The producer runs only when collected.
- How to fix it
- Collect within an owned scope or choose an explicit sharing lifecycle.
- Prevent it next time
- Document cold versus hot behavior at stream boundaries.
A StateFlow consumer misses intermediate events.
- Why it happens
- Current-state conflation was mistaken for an event log.
- How to fix it
- Use an event-delivery design matching required retention and delivery semantics.
- Prevent it next time
- Classify data as current state or required individual events.
A mutable state object changes without the expected emission.
- Why it happens
- Shared in-place mutation undermines equality-based change detection.
- How to fix it
- Create a new immutable state value under the owner’s update policy.
- Prevent it next time
- Avoid mutable collections inside published state snapshots.
Check your understanding
1. Is conflate appropriate for a sequence of seat-decrement commands?
Show worked solution
Generally no, because dropping an intermediate command changes the quantity and business effect. Conflation can suit a current display state such as the latest progress, where intermediate states are intentionally replaceable. Define the required event semantics before using an operator that skips values.
2. Why should MutableStateFlow.update avoid sending mail inside its lambda?
Show worked solution
The update function may evaluate the lambda more than once during concurrent contention, so an external effect could repeat. Keep the transformation pure and perform effects through an explicitly owned workflow with its own identity and error policy. Atomic state replacement is not an atomic transaction with a mail provider.
Key takeaways
- Cold Flow, hot StateFlow, and durable events serve different contracts.
- Buffering, cancellation, and conflation must preserve the meaning of the data.