Phase 4 · Data & PersistenceModule 24~28 min read

Repository Pattern & Data Layer

Build a clean data layer that combines local and remote sources behind a single repository.

What you'll learn

You can read from Room and from Retrofit — but a ViewModel shouldn't juggle both directly. The repository is the clean boundary that combines your data sources behind one simple interface. This is the piece that makes the "Model" layer of MVVM real, and it's where offline-capable apps come from.

By the end you'll be able to:

  • Explain why apps need a dedicated data layer
  • Design a repository as an interface
  • Combine local (Room) and remote (Retrofit) sources
  • Apply the single source of truth principle to data

Why a data layer

If ViewModels called Room and Retrofit directly, data logic would scatter across every screen — caching rules duplicated, no clear place to decide "network or cache?", and impossible to test without a real database and server. A data layer gathers all of that in one place. The ViewModel asks the repository for data and doesn't care where it comes from.

The repository

A repository owns one type of data (articles, tasks, users) and exposes clean operations for it. Define it as an interface so the ViewModel depends on the abstraction, not the implementation — that swap is what makes testing (and Hilt, in Phase 6) easy:

ArticleRepository.kt
// The repository is an interface — the ViewModel depends on this abstraction.
interface ArticleRepository {
    fun observeArticles(): Flow<List<Article>>
    suspend fun refresh()
}

Key idea

Depending on an interface means you can hand the ViewModel a real repository in production and a fake one in tests — no network, no database, instant and deterministic. This one habit unlocks testable architecture.

Combining local & remote

The implementation wires the sources together. The classic pattern: the UI always reads from the local database (fast, works offline), and a refresh() fetches from the network and writes into that database. Because the database read is a Flow, the UI updates itself the moment fresh data lands:

DefaultArticleRepository.kt
class DefaultArticleRepository(
    private val dao: ArticleDao,      // local (Room)
    private val api: NewsApi,         // remote (Retrofit)
) : ArticleRepository {

    // The UI always reads from the database — the single source of truth.
    override fun observeArticles(): Flow<List<Article>> =
        dao.observeAll().map { list -> list.map { it.toDomain() } }

    // Refresh fetches from the network and updates the database.
    override suspend fun refresh() {
        val fromNetwork = api.getHeadlines()
        dao.upsertAll(fromNetwork.map { it.toEntity() })
        // No manual UI update needed — the Flow above re-emits automatically.
    }
}

The ViewModel becomes wonderfully simple — observe, and trigger a refresh:

NewsViewModel.kt
class NewsViewModel(private val repository: ArticleRepository) : ViewModel() {

    val articles: StateFlow<List<Article>> = repository.observeArticles()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

    init { refresh() }
    fun refresh() = viewModelScope.launch { runCatching { repository.refresh() } }
}

Single source of truth

Notice the crucial decision: the database is the single source of truth. The network never feeds the UI directly — it only updates the database, and the UI observes the database. This gives you three wins for free:

  • Offline support — the last-known data is always on screen, even with no signal.
  • Consistency — there's exactly one copy of the truth; no conflicting caches.
  • Reactivity — a single write updates every screen observing that data.

Tip

This closes Phase 4. Trace the full flow: Retrofit → repository → Room → Flow → ViewModel → StateFlow → Compose. Every arrow is something you learned in this phase — and together they're the architecture behind virtually every professional Android app.

Recap & quick check

Key takeaways

  • A data layer centralizes data logic so ViewModels don't call Room/Retrofit directly.
  • Define repositories as interfaces; the ViewModel depends on the abstraction, enabling fakes in tests.
  • Classic pattern: the UI reads from the local database; refresh() fetches from the network into it.
  • Make the database the single source of truth — the network updates it; the UI observes it.
  • This yields offline support, consistency, and automatic reactivity via Flow.

Quick check

1. Why introduce a repository layer?

2. Why define a repository as an interface?

3. In the offline-first pattern, where does the UI read from?

4. What is the 'single source of truth' here?

5. A benefit of the network updating the DB rather than the UI directly is…

That completes Phase 4 — you can build the full data layer of a real app! 🎉 In Phase 5 we polish the UI with side effects, animations, custom layouts, and performance.