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.

Note: 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.
Up next: TrailApiService has been referenced twice now without ever being defined. Retrofit + Coroutines builds that exact interface — the actual networking half of this repository.