Explain the Repository pattern and "single source of truth" on Android
A Repository hides where data comes from; single source of truth decides which copy of that data the UI is actually allowed to trust — together they're why offline-first apps don't fall apart.
LEARN BY DOINGPut your intuition to work3 steps
Explain the Repository pattern and "single source of truth" on Android
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#What a Repository actually does
A Repository is a class that sits between "where data physically lives" (a REST API, a Room database, DataStore, an in-memory cache) and "who consumes it" (a ViewModel, a use case). It exposes a small, intention-revealing API — observeArticles(), refresh(), getArticle(id) — and hides everything about how that data is fetched, cached, or combined behind it. The ViewModel calling repository.observeArticles() has no idea, and shouldn't need to, whether that data came from a network call five seconds ago or from Room.
This buys you two things that matter in an interview answer, not just in theory: testability — a ViewModel test fakes the Repository interface instead of standing up Retrofit and Room — and a seam for dependency inversion — the ViewModel depends on ArticleRepository (an interface), not ArticleRepositoryImpl, so the concrete wiring (which API, which database) lives at the Hilt/Dagger module boundary, not scattered through the UI layer.
interface ArticleRepository {fun observeArticles(): Flow<List<Article>>suspend fun refresh()}
#What "single source of truth" adds on top
The Repository pattern alone doesn't say which underlying source the UI should trust when the network and the local cache disagree — that's what single source of truth (SSOT) answers. The rule: pick exactly one place data lives canonically — almost always the local database — and everything else (network responses, in-memory caches) exists only to update that canonical copy. The UI never observes the network directly; it observes the database, and the database is what the network writes into.
class ArticleRepositoryImpl @Inject constructor(private val dao: ArticleDao,private val api: ArticleApi,) : ArticleRepository {override fun observeArticles(): Flow<List<Article>> =dao.observeAll().map { entities -> entities.map { it.toDomain() } }override suspend fun refresh() {val remote = api.fetchArticles()dao.upsertAll(remote.map { it.toEntity() })}}@Daointerface ArticleDao {@Query("SELECT * FROM articles ORDER BY publishedAt DESC")fun observeAll(): Flow<List<ArticleEntity>>@Upsertsuspend fun upsertAll(articles: List<ArticleEntity>)}
Notice observeArticles() never touches api at all — it's a Flow straight out of Room. refresh() is the only function that talks to the network, and its entire job is to write the result into Room. Room's Flow then emits automatically because Room invalidates and re-queries whenever the underlying table changes — the UI gets the update "for free" without the Repository needing to manually push anything to it.
#Wiring it into a ViewModel
class ArticleListViewModel @Inject constructor(private val repository: ArticleRepository,) : ViewModel() {val articles: StateFlow<List<Article>> =repository.observeArticles().stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())init {viewModelScope.launch { repository.refresh() }}}
articles is always backed by Room, so if refresh() fails (no network), the UI still shows the last cached list instead of an empty screen — that's the offline-first payoff of SSOT, and it falls out naturally from the design rather than needing special-case error-state code.