ViewModel & LiveData

Back in the lifecycle lesson, you saw that rotating the device destroys and recreates an Activity or Fragment by default. onSaveInstanceState() can rescue a handful of small values, but it's the wrong tool for something like TrailLog's current in-memory list of trails — you don't want to serialize an entire list into a Bundle on every rotation just to survive it. ViewModel exists to solve exactly this problem.

The Problem: Data Held in the UI Layer Doesn't Survive

If TrailListFragment keeps its list of trails in a plain property, that list is gone the instant the Fragment is recreated — the user rotates their phone and the screen briefly flashes empty before reloading. A ViewModel is a class specifically designed to survive configuration changes automatically, scoped to the lifecycle of its owning Activity or Fragment rather than to the specific instance that gets destroyed and rebuilt. The system keeps the same ViewModel instance alive across the rotation and simply hands it back to the new Fragment instance.

Worked Example: TrailListViewModel

class TrailListViewModel : ViewModel() {

    private val _trails = MutableLiveData<List<Trail>>(emptyList())
    val trails: LiveData<List<Trail>> = _trails

    fun loadTrails(loadedTrails: List<Trail>) {
        _trails.value = loadedTrails
    }
}

A Fragment obtains its ViewModel through the by viewModels() Kotlin property delegate, rather than constructing it directly:

class TrailListFragment : Fragment() {

    private val viewModel: TrailListViewModel by viewModels()

    // ...
}

The by keyword here is Kotlin's property delegate syntax — instead of viewModel holding the value directly, viewModels() hands back a small helper object that knows how to create (or, after a rotation, simply retrieve) the correct TrailListViewModel instance scoped to this Fragment, and every read of viewModel goes through that helper. It's a genuinely elegant piece of Kotlin: what would otherwise be several lines of manual ViewModelProvider lookup collapses into one property declaration.

LiveData: Observing Data That Knows About the UI's Lifecycle

A plain callback has no idea whether the screen listening to it is still alive — if TrailListFragment is destroyed while a background load is still running, a raw callback firing afterward tries to update views that no longer exist, which is a common source of crashes and memory leaks in hand-rolled async code. LiveData is an observable data holder that is lifecycle-aware: it only delivers updates to observers that are currently in an active state, and it automatically stops delivering them — and cleans up the subscription — once the observing Fragment or Activity is destroyed. That single property eliminates an entire category of "updated a dead view" crashes without any manual unsubscribe code.

Worked Example: Observing the Trail List

TrailListViewModel above already exposes trails as LiveData<List<Trail>>. The Fragment observes it like this:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewModel.trails.observe(viewLifecycleOwner) { trails ->
        trailAdapter.submitList(trails)
    }
}

Passing viewLifecycleOwner — not the Fragment itself — is important here: it ties the observation to the Fragment's view lifecycle rather than the Fragment object's own longer lifecycle, which matters because, as covered in the Fragments lesson, a Fragment's view can be destroyed and recreated while the Fragment instance itself lives on.

MutableLiveData Internally, Plain LiveData Externally

Look again at the two properties on TrailListViewModel: a private _trails of type MutableLiveData, and a public trails of the plain, read-only LiveData type, simply assigned from the mutable one. This is a genuinely common and worthwhile encapsulation pattern — only the ViewModel itself can call .value = to change the data, while anything observing from outside, like the Fragment, can only read it. Without this split, any Fragment holding a reference to a mutable LiveData could push its own values into it, defeating the entire point of the ViewModel owning the data.

Note: As throughout this track, this is correct, static Kotlin code — this website's own project doesn't run a live Android Studio build, so it isn't executing on this page.
Looking ahead: LiveData has served Android well for years, but it predates Kotlin's own Flow APIs. Flow vs LiveData, later in this track, introduces StateFlow as LiveData's more modern, Kotlin-native alternative — and explains when you'd reach for each.