The Repository Pattern
Every data lesson so far has looked at one storage mechanism in isolation — DataStore, Room, files.
A real app rarely gets that luxury: TrailLog's trail list might need to come from a local Room
cache and a network API, and the code deciding which one to use, and when, has to live
somewhere. The Repository pattern is the answer to exactly that question — and it's the piece that
finally turns TrailLog's TrailViewModel from earlier lessons into something backed by
real data.
The Problem: A ViewModel Shouldn't Know Where Data Comes From
Think back to Architecture: MVVM — the ViewModel's job
is to hold UI state and expose it to the screen, not to manage databases or HTTP clients directly.
If TrailViewModel called Room and Retrofit itself, it would need to know about
caching, network failures, and data merging — concerns that have nothing to do with "what should
the trail list screen show right now." Every one of those concerns would also have to be
duplicated in any other ViewModel that also needed trail data.
A Repository is a class whose entire job is to hide that complexity: it exposes a small, simple API ("give me the trails"), and internally decides whether that means reading Room, calling the network, or both. The ViewModel talks only to the repository and never has to care which storage mechanism actually served the data.
A Real TrailRepository
Here is a genuinely realistic, offline-friendly TrailRepository for TrailLog. It takes
a Room DAO and a Retrofit API service as constructor dependencies, and exposes one suspend function
that tries the network first and quietly falls back to the local cache if that fails:
class TrailRepository(
private val trailDao: TrailDao,
private val api: TrailApiService
) {
suspend fun getTrails(): List<Trail> {
return try {
val freshTrails = api.getTrails()
trailDao.insertAll(freshTrails) // refresh the local cache on success
freshTrails
} catch (e: Exception) {
// no connection, timeout, or server error — fall back to whatever we have cached
trailDao.getAllTrails()
}
}
}
Nothing about the method signature — suspend fun getTrails(): List<Trail> — tells
the caller anything about Room or Retrofit being involved. That's the entire point: the network
call and the cache lookup are an implementation detail the repository owns, not something callers
need to reason about.
The ViewModel's Job Gets Radically Simpler
With TrailRepository in place, TrailViewModel's data-loading code shrinks
to a single call — it doesn't know or care that a network request or a database read even
happened:
class TrailViewModel(private val repository: TrailRepository) : ViewModel() {
private val _trails = MutableStateFlow<List<Trail>>(emptyList())
val trails: StateFlow<List<Trail>> = _trails
fun loadTrails() {
viewModelScope.launch {
_trails.value = repository.getTrails() // that's the whole thing
}
}
}
If tomorrow TrailLog adds a third data source — say, a paginated endpoint, or a local search index —
only TrailRepository changes. TrailViewModel, and every screen built on
top of it, stays exactly as it is.
This Is the "Model" in MVVM, Made Concrete
The MVVM lesson described the Model layer only in the abstract, as "wherever the data actually
lives." TrailRepository is that description turned into real, working code — it's the
Model layer's concrete implementation, sitting between the ViewModel and the actual data sources.
UI (Activity / Fragment)
↓↑
ViewModel
↓↑
TrailRepository
↓↑ ↓↑
Room TrailDao Retrofit TrailApiService
Data (and requests for data) flow straight down this chain, and results flow straight back up. Each layer only ever talks to its direct neighbor: the UI never touches the repository directly, and the repository never touches the UI.
Why Constructor Injection Matters Here
Notice TrailRepository receives its TrailDao and TrailApiService
as constructor parameters rather than creating them itself. That's what makes it possible to swap in
fake versions of both during testing — a pattern this track builds on properly once dependency
injection is introduced in Dependency Injection with
Hilt, so nothing here needs to be wired up by hand forever.
TrailRepository and TrailViewModel above are correct, real Kotlin/Android code — but this website's own project doesn't run a live Android Studio build, so nothing here executes on the page itself.TrailApiService has been referenced twice now without ever being defined. Retrofit + Coroutines builds that exact interface — the actual networking half of this repository.