Flow vs LiveData
The previous lesson covered coroutines returning a single result. But a lot of what TrailLog needs
is a continuous stream of results — the trail list should update on screen the moment a new hike is
saved to the database, without the user having to manually refresh anything. Flow is
Kotlin's own answer to that: a stream of values over time, built directly on the same coroutines
foundation from the last lesson. That's a genuinely important distinction from LiveData,
which is a separate, Android-specific library with no direct tie to coroutines at all. This lesson
covers both, honestly compares them, and shows the exact conversion from one to the other on
TrailLog's own ViewModel.
Flow: A Coroutine-Based Stream of Values
Where a suspend fun returns one value, a function returning Flow<T>
can emit many values over time — for example, a fresh List<Trail> every time the
underlying Room database table changes:
fun observeTrails(): Flow<List<Trail>> {
// Room can return a Flow directly from a DAO query — it re-emits
// automatically whenever the underlying table's data changes
return trailDao.observeAllTrails()
}
A Flow does nothing on its own until something collects it. Collecting
happens inside a coroutine, typically launched in a lifecycle-aware scope like
viewLifecycleOwner.lifecycleScope in a Fragment:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.observeTrails().collect { trails ->
// Runs every time a new list is emitted, e.g. after saving a new hike
adapter.submitList(trails)
}
}
StateFlow: the Flow Equivalent of LiveData
A plain Flow is "cold" — it only starts producing values once something collects it,
and it has no concept of a "current value" you can just read. StateFlow is a special
kind of Flow built specifically to behave like LiveData: it always holds a
current value, new collectors immediately get the latest value on collection, and it's safe to
collect straight from the UI layer.
Here's TrailLog's TrailListViewModel from
ViewModel & LiveData, converted from
LiveData<List<Trail>> to StateFlow<List<Trail>>:
class TrailListViewModel(private val repository: TrailRepository) : ViewModel() {
private val _trails = MutableStateFlow<List<Trail>>(emptyList())
val trails: StateFlow<List<Trail>> = _trails
fun loadTrails() {
viewModelScope.launch {
_trails.value = withContext(Dispatchers.IO) {
repository.loadTrails()
}
}
}
}
The shape of the class barely changes — MutableLiveData becomes
MutableStateFlow, LiveData becomes StateFlow, and
.value = still works exactly the same way to publish a new state.
Collecting Safely: repeatOnLifecycle
Here's the one genuinely important extra detail Flow asks of you that LiveData handles
automatically: StateFlow/Flow collection isn't lifecycle-aware on its own,
so collecting naively (like the plain lifecycleScope.launch shown earlier) can keep
collecting — and doing UI work — even while an Activity or Fragment is stopped and not visible. The
fix is wrapping collection in repeatOnLifecycle, which automatically starts collecting
when the lifecycle reaches the given state and cancels collection when it drops below it, restarting
cleanly each time:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.trails.collect { trails ->
adapter.submitList(trails)
}
}
}
That single wrapper is the practical difference between "Flow collection that might waste work (or worse, touch views) while the screen is in the background" and "Flow collection that behaves exactly as safely as LiveData always did."
Honest Comparison: LiveData vs. StateFlow/Flow
| LiveData | StateFlow / Flow | |
|---|---|---|
| Simplicity | Simpler — nothing extra to opt into | A bit more setup (repeatOnLifecycle) for full safety |
| Platform | Android-specific library | Plain Kotlin coroutines — works in any Kotlin codebase, not just Android |
| Lifecycle awareness | Automatic — built in from the start | Manual, via repeatOnLifecycle — an extra step, but explicit and flexible |
| Operators | Limited transformation options | Rich operators — map, filter, combine, debounce, and more, directly on the stream |
| Multiple streams | Awkward to combine several LiveData sources | combine() merges multiple Flows cleanly |
StateFlow for new code,
largely because of its richer operators and the fact that it's not tied to the Android framework.
That said, LiveData remains completely valid, is still fully supported, and you will
see both used side by side in real, actively-maintained codebases — including older parts of apps
that were written before StateFlow became the default recommendation. Knowing both,
and knowing how to convert between them as shown above, matters more than picking a side.