Kotlin Flow Cheat Sheet
Building, transforming, and collecting cold asynchronous streams with Kotlin Flow, plus StateFlow/SharedFlow for hot state.
Building & Collecting a Flow
Flow is cold: the block only runs when collected, and runs fresh per collector.
import kotlinx.coroutines.flow.*import kotlinx.coroutines.*fun numbers(): Flow<Int> = flow { for (i in 1..5) { delay(100) // suspend inside the builder is fine emit(i) // push a value downstream }}suspend fun main() { numbers().collect { value -> println(value) // 1, 2, 3, 4, 5 -- printed as they're emitted }}// flowOf and asFlow for simple sourcesflowOf(1, 2, 3)listOf(1, 2, 3).asFlow()
Transforming Operators
Flow supports familiar sequence-like operators, all suspend-aware.
numbers() .map { it * it } .filter { it % 2 == 0 } .take(3) .onEach { println("saw $it") } .catch { e -> println("caught: $e") } // catches upstream exceptions .collect { println("result: $it") }// flatMapConcat / flatMapMerge / flatMapLatest for flows-of-flowssearchQuery .debounce(300) .flatMapLatest { query -> searchApi(query) } // cancels previous search on new input .collect { results -> render(results) }
Context Preservation & flowOn
Flow preserves the collector's context; flowOn changes upstream dispatch.
fun heavyWork(): Flow<Int> = flow { for (i in 1..5) { Thread.sleep(50) // pretend CPU-bound work emit(i) }}.flowOn(Dispatchers.Default) // upstream (the flow{} builder) runs on Defaultsuspend fun run() { withContext(Dispatchers.Main) { heavyWork().collect { value -> updateUI(value) // collect lambda runs on Main, as expected } }}
StateFlow & SharedFlow
Hot flows for UI state (StateFlow) and event broadcasting (SharedFlow).
class CounterViewModel : ViewModel() { private val _count = MutableStateFlow(0) // requires an initial value val count: StateFlow<Int> = _count.asStateFlow() fun increment() { _count.update { it + 1 } // atomic update }}class EventBus { private val _events = MutableSharedFlow<String>(replay = 0, extraBufferCapacity = 1) val events: SharedFlow<String> = _events.asSharedFlow() suspend fun publish(event: String) = _events.emit(event)}// Collecting StateFlow in Composeval count by viewModel.count.collectAsStateWithLifecycle()
StateFlow vs SharedFlow vs Flow
Choosing the right stream type.
- Flow- cold, no state, runs fresh per collector, for one-shot async sequences
- StateFlow- hot, always has a current value, conflates rapid updates, ideal for UI state
- SharedFlow- hot, configurable replay/buffer, ideal for one-off events (snackbars, navigation)
- channelFlow- builder for flows that need concurrent emit() from multiple coroutines
- stateIn / shareIn- convert a cold Flow into a hot StateFlow/SharedFlow scoped to a CoroutineScope
Combining Multiple Flows
combine emits whenever any source updates (using each flow's latest value); zip pairs elements positionally; merge interleaves emissions as they arrive.
val name: Flow<String> = flowOf("Ada", "Grace")val age: Flow<Int> = flowOf(30, 40)// combine: recomputes with the LATEST value from each flow on every emissionname.combine(age) { n, a -> "$n is $a" } .collect { println(it) }// zip: pairs by index, completes when the shorter flow completesname.zip(age) { n, a -> "$n/$a" } .collect { println(it) }// merge: interleaves emissions from multiple flows of the same typeval fast = flow { emit(1); delay(10); emit(2) }val slow = flow { delay(5); emit(100) }merge(fast, slow).collect { println(it) } // order depends on timing
Backpressure: buffer, conflate, collectLatest
Control how a slow collector handles a fast emitter without blocking the upstream producer.
flow { for (i in 1..100) { delay(10) emit(i) // fast producer }}.buffer(capacity = 50) // decouples producer/consumer with a channel.collect { slowConsume(it) } // producer no longer waits on consumer// conflate: drops intermediate values, keeps only the latest when consumer is behindsensorReadings.conflate().collect { render(it) }// collectLatest: cancels the in-progress collector block when a new value arrivessearchQuery.collectLatest { query -> val results = searchApi(query) // canceled mid-flight if `query` changes again render(results)}
callbackFlow for Callback-Based APIs
Wraps listener/callback APIs (SDKs, Firebase, sensors) into a cold Flow, with awaitClose for cleanup.
fun locationUpdates(client: LocationClient): Flow<Location> = callbackFlow { val callback = object : LocationCallback() { override fun onLocationChanged(loc: Location) { trySend(loc) // non-suspending, safe from any thread .onFailure { /* channel closed/full */ } } } client.requestUpdates(callback) awaitClose { client.removeUpdates(callback) // guaranteed to run when the collector cancels }}// Contrast with the simpler `flow { }` builder: callbackFlow backs onto a// Channel, so trySend() can be called from a non-suspending callback thread.
retryWhen & Exception Transparency
catch only intercepts upstream exceptions; retryWhen re-subscribes upstream based on a predicate, commonly with exponential backoff.
fun fetchData(): Flow<Data> = flow { emit(api.getData()) // throws on network failure}.retryWhen { cause, attempt -> if (cause is IOException && attempt < 3) { delay(1000L * (attempt + 1)) // simple linear backoff true // true = retry, false = rethrow } else { false }}.catch { e -> emit(Data.Empty) } // final fallback after retries are exhausted// Exception transparency rule: never try/catch around emit() inside a flow{}// builder -- it breaks cancellation semantics. Let exceptions propagate and// handle them with .catch{} downstream instead.
Advanced Flow Operator Reference
Operators beyond the map/filter basics that show up in production Android/Kotlin code.
- distinctUntilChanged()- suppresses consecutive duplicate emissions, common after combine() to avoid redundant UI updates
- transform { }- general-purpose operator that can emit zero, one, or many values per upstream item (map/filter are built on it)
- channelFlow- like callbackFlow but general-purpose, for concurrent emit() from multiple coroutines launched inside the builder
- onCompletion { }- runs on normal completion, cancellation, or exception -- the Flow analogue of try/finally
- first() / firstOrNull()- terminal operator that collects one value then cancels the upstream flow
- flatMapConcat vs flatMapMerge- Concat preserves order and processes sequentially; Merge runs inner flows concurrently, interleaving output
- SharingStarted.WhileSubscribed(5000)- keeps a shared/stateIn flow active for 5s after the last collector leaves, avoiding restart thrash on config changes
Don't put one-off events (like 'show a toast') in a StateFlow — because it always replays its latest value to new collectors, a screen rotation or re-subscription will re-fire the last event; use SharedFlow with replay=0 for events, and reserve StateFlow strictly for durable state.