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:
// The repository is an interface — the ViewModel depends on this abstraction.
interface ArticleRepository {
fun observeArticles(): Flow<List<Article>>
suspend fun refresh()
}Key idea
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:
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:
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
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.