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

LiveDataStateFlow / Flow
SimplicitySimpler — nothing extra to opt intoA bit more setup (repeatOnLifecycle) for full safety
PlatformAndroid-specific libraryPlain Kotlin coroutines — works in any Kotlin codebase, not just Android
Lifecycle awarenessAutomatic — built in from the startManual, via repeatOnLifecycle — an extra step, but explicit and flexible
OperatorsLimited transformation optionsRich operators — map, filter, combine, debounce, and more, directly on the stream
Multiple streamsAwkward to combine several LiveData sourcescombine() merges multiple Flows cleanly
Honest recommendation: Google itself now recommends 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.
Next up: That wraps up the Kotlin Language Deep Dive section. Layout Overview starts the next part of the track — how TrailLog's actual screens get built, starting with the View/ViewGroup hierarchy XML layouts describe.