What you'll learn
Some data doesn't arrive once — it streams. A database table changes, a location updates, a search query evolves keystroke by keystroke. Kotlin Flow models a stream of values over time, and it's how Room and DataStore hand you data. You've seen StateFlow already; now let's use flows properly.
By the end you'll be able to:
- Distinguish a cold Flow from
StateFlow/SharedFlow - Transform a flow with operators like
mapandfilter - Combine several streams into one
- Turn a flow into UI state and collect it safely
Cold flows vs StateFlow / SharedFlow
The three kinds you'll meet, and when each fits:
| Type | Nature | Typical use |
|---|---|---|
Flow<T> | Cold — runs only while collected; no current value | A stream from Room/DataStore or the network |
StateFlow<T> | Hot — always has a current value, emits on change | UI state exposed by a ViewModel |
SharedFlow<T> | Hot — no stored value; broadcasts to collectors | One-off events (navigate, show snackbar) |
A cold flow is lazy: nothing happens until someone collects it, and it starts fresh for each collector — like a recipe that only cooks when ordered. A hot flow (StateFlow/SharedFlow) is already running and shared among collectors — like a live radio broadcast.
Note
StateFlow; use SharedFlow for one-time events that shouldn't replay on rotation (a "saved!" snackbar shouldn't fire again just because the screen was recreated).Transforming flows
Flows compose like collections. Chain operators to build a pipeline — map to reshape each value, filter to drop some, distinctUntilChanged to skip duplicates. The transformation runs each time the source emits, keeping downstream always up to date.
Combining streams
combine merges several flows: whenever any source emits, it recomputes a result from the latest value of each. This is perfect for search — combine the live list of contacts with the current query to produce a filtered list — and stateIn converts that cold pipeline into a StateFlow the UI can read:
class SearchViewModel(private val repo: ContactRepository) : ViewModel() {
private val query = MutableStateFlow("")
fun onQueryChange(q: String) { query.value = q }
// A pipeline: raw contacts flow → filtered by the current query → UI state.
val uiState: StateFlow<List<Contact>> =
combine(repo.contacts, query) { contacts, q ->
contacts.filter { it.name.contains(q, ignoreCase = true) }
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList(),
)
}Tip
stateIn(..., SharingStarted.WhileSubscribed(5_000), initialValue) is the standard recipe for exposing a derived flow as UI state. WhileSubscribed(5000) keeps the upstream alive for 5 seconds after the last collector leaves — long enough to survive a rotation, short enough to stop work when the user really leaves.Collecting safely
In Compose, never collect a flow with a raw loop. Convert it to state with collectAsStateWithLifecycle() — it collects only while the screen is on-screen and gives you a value that triggers recomposition on change:
@Composable
fun SearchScreen(viewModel: SearchViewModel) {
val contacts by viewModel.uiState.collectAsStateWithLifecycle()
LazyColumn {
items(contacts, key = { it.id }) { ContactRow(it) }
}
}That's the whole reactive loop: the database emits new contacts → the pipeline re-filters → the StateFlow updates → the list recomposes. You wrote no manual refresh anywhere.
Recap & quick check
Key takeaways
- Flow models a stream of values over time — how Room and DataStore deliver data.
- Cold Flow runs per-collector and has no current value; StateFlow is hot with a current value; SharedFlow broadcasts events.
- Expose renderable state as StateFlow; use SharedFlow for one-off events that shouldn't replay.
- Transform with map/filter and merge streams with combine; convert to state with stateIn.
- In Compose, collect with collectAsStateWithLifecycle() — never a raw collect loop.
Quick check
1. What is a cold Flow?
2. Which type best fits UI state a screen renders?
3. You want a filtered list from a data stream plus a query stream. Use…
4. What does stateIn(..., WhileSubscribed(5000)) do?
5. Best way to fire a one-off 'Saved!' event that shouldn't replay on rotation?
You can stream data reactively. Now let's persist it — starting with simple settings via DataStore.