Kotlin Flow and Cold Streams
What is Flow?
Flow is Kotlin's answer to reactive streams. It produces a sequence of values asynchronously using suspend functions. Unlike RxJava's Observable, Flow is cold — the producer code does not run until someone collects it.
fun fetchItems(): Flow<Item> = flow {
val items = api.getItems() // suspends until data arrives
items.forEach { item ->
emit(item) // sends one item to the collector
delay(100) // simulate processing
}
}
// Nothing happens here — flow is cold
val itemsFlow = fetchItems()
// Now the flow runs — collector triggers production
lifecycleScope.launch {
itemsFlow.collect { item ->
println(item.name)
}
}
Flow Operators
Operators transform flows before or after collection. They fall into two categories:
Intermediate operators (transform the stream):
api.searchFlow(query)
.debounce(300L) // wait for typing to stop
.distinctUntilChanged() // skip duplicate queries
.map { it.results } // extract only results
.filter { it.isNotEmpty() } // skip empty results
.catch { emit(emptyList()) } // handle upstream errors
.collect { results ->
_uiState.value = results
}
Terminal operators (trigger collection):
collect— consume each emissiontoList— collect all values into a listfirst— get the first emission and cancelreduce— accumulate values into one result
Building Flows
// From values
val numbers = flowOf(1, 2, 3, 4, 5)
// From a sequence
val seq = sequenceOf("a", "b", "c").asFlow()
// From a suspend function
fun temperatureStream(): Flow<Float> = flow {
while (true) {
emit(sensor.readTemperature())
delay(1000)
}
}
// From callback
fun callbackFlow(): Flow<Event> = callbackFlow {
val listener = object : EventListener {
override fun onEvent(event: Event) {
trySend(event) // non-blocking send
}
}
eventSource.register(listener)
awaitClose { eventSource.unregister(listener) } // cleanup
}
Flow vs suspend Functions
A suspend function returns one value. A Flow emits multiple values over time. Use Flow when you need a stream; use suspend when you need a single result.
Quiz
1. What makes a Kotlin Flow "cold"?
2. When should you use StateFlow over SharedFlow?
3. Why use `repeatOnLifecycle` when collecting flows in a Fragment?
4. What does `distinctUntilChanged()` do on a StateFlow?
Flashcards
Question
What is the difference between a cold flow and a hot flow?
Click to reveal answer
Answer
A cold flow runs its producer code only when collected. A hot flow (StateFlow, SharedFlow) emits values regardless of whether anyone is collecting.
Question
When would you use `callbackFlow`?
Click to reveal answer
Answer
When wrapping callback-based APIs (like Firebase listeners or broadcast receivers) into a Flow. Use awaitClose to clean up the listener when collection stops.
Question
Why must StateFlow use data classes or override equals()?
Click to reveal answer
Answer
StateFlow only emits when the new value is not equal to the current value. Without proper equals(), every emission triggers a new value even if nothing changed.
Question
What terminal operator should you avoid on a long-lived flow?
Click to reveal answer
Answer
Operators like first(), toList(), or reduce() that complete after one or a few emissions. They cancel the upstream flow. Use collect() for continuous streams.
Revision Notes
Key Takeaways
- 1. Flow is cold — producer runs only when collected; hot flows (StateFlow, SharedFlow) emit regardless
- 2. StateFlow is for UI state that always has a value; SharedFlow is for one-time events
- 3. Use repeatOnLifecycle to collect flows only when the UI is visible
- 4. StateFlow uses equals() to suppress duplicate emissions — use data classes
Interview Tips
- • Explain cold vs hot flows using the function vs observable analogy
- • Know when to use StateFlow (state) vs SharedFlow (events) — a common design question
- • Be ready to explain callbackFlow and how it bridges callback APIs to coroutines
- • Mention repeatOnLifecycle as a best practice for Android flow collection
Cheat Sheet
Flow & SharedFlow Cheat Sheet
Flow (Cold):
- Producer runs on collection
- Lazy — no work until collected
- Terminated by: collect, first, toList, reduce
StateFlow (Hot):
- Always has a current value
- Only emits on change (equals check)
- replay = 1 — new collectors get current value
- Use for UI state in ViewModels
SharedFlow (Hot):
- No required initial value
- Configurable replay buffer
- Use for one-time events (navigation, toasts)
Key Operators:
- debounce — delay emissions until pauses
- distinctUntilChanged — skip duplicates
- map/filter/transform — intermediate transforms
- catch — handle upstream errors
- combine — merge latest values from multiple flows
Android Collection:
- repeatOnLifecycle(STARTED) — collect only when visible
- Always collect in lifecycleScope or viewModelScope