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.

──observes──▶

ViewModel

Holds UI state & logic. Exposes LiveData/StateFlow.

──reads──▶

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:

LayerTrailLog pieceResponsibility
ViewTrailListFragmentInflates its layout, observes viewModel.trails, submits the list to a RecyclerView adapter.
ViewModelTrailListViewModelHolds the current list as LiveData<List<Trail>>, decides when and how it changes.
ModelA future TrailRepositoryActually 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.

Note: As with every lesson in this track, the code and structure shown here are correct and static — this website's own project doesn't run a live Android Studio build, so nothing executes on the page itself.
Next up: Before going further into Jetpack and TrailLog's architecture, it's worth tightening up Kotlin itself. Null Safety & Scope Functions covers one of Kotlin's most distinctive features — the one that eliminates most NullPointerException crashes by design.