Architecture: MVVM
The previous two lessons introduced TrailListFragment and
TrailListViewModel without naming the pattern behind that split. That pattern is
MVVM — Model-View-ViewModel — and it's not just one option among many for this
track: it's the architecture Google's own Jetpack libraries (ViewModel, LiveData, the Navigation
Component you just learned) are explicitly designed around, so it's the one we'll use
consistently from here on.
The Three Layers
View
Activity / Fragment. Displays UI, forwards user actions.
ViewModel
Holds UI state & logic. Exposes LiveData/StateFlow.
Model
The actual data — Room database, network layer.
Data flows in one direction that's easy to keep straight: the Model is the source of truth — for TrailLog, eventually a Room database and a network layer, tied together properly by the Repository pattern covered in The Repository Pattern later in this track. The ViewModel pulls from the Model, applies whatever logic the screen needs, and exposes the result as observable state. The View — an Activity or Fragment — does nothing more than render whatever the ViewModel currently exposes and forward user actions (a tap, a text change) back to it. User input flows up from the View into the ViewModel; state flows back down from the ViewModel into the View.
Why This Separation Genuinely Helps
The most immediate payoff is that the View becomes dumb on purpose — it holds almost no
decision-making logic of its own, just display logic and simple forwarding. That has a very
practical consequence: the ViewModel's logic can be unit-tested completely on its own, without
spinning up any Android UI framework, an emulator, or a device at all. A test can construct a
TrailListViewModel, feed it fake trail data, and assert on what its
LiveData exposes — nothing about that requires an actual screen to exist.
Unit Testing (JUnit & MockK), later in this track,
builds directly on this property of MVVM.
Worked Example: TrailListFragment and TrailListViewModel Together
Putting the two previous lessons' pieces into the three-layer picture:
| Layer | TrailLog piece | Responsibility |
|---|---|---|
| View | TrailListFragment | Inflates its layout, observes viewModel.trails, submits the list to a RecyclerView adapter. |
| ViewModel | TrailListViewModel | Holds the current list as LiveData<List<Trail>>, decides when and how it changes. |
| Model | A future TrailRepository | Actually supplies the List<Trail> data, from Room, the network, or both. |
Right now TrailListViewModel is fed its trail list directly through
loadTrails(). Once the Repository pattern lesson introduces a proper
TrailRepository, the ViewModel will pull from that repository instead — the View and
the observation code you already wrote won't need to change at all, which is exactly the kind of
flexibility this layering is meant to buy you.
MVVM Isn't the Only Option
It's worth being honest here: MVVM isn't the single correct way to structure an Android app. Some teams prefer MVI (Model-View-Intent), which models the entire screen as one immutable state object plus a stream of user "intents" that produce new states — a stricter, more unidirectional variant that some codebases find easier to reason about at scale. We won't go deep on MVI in this track. MVVM is what we'll use consistently going forward simply because it's what Jetpack's own libraries — ViewModel, LiveData, StateFlow, Navigation — are built to support directly, which means less friction fighting the framework and more time building TrailLog itself.
NullPointerException crashes by design.