Repository Pattern: The True Data Abstraction
Table of Contents
🏛️ Theory: The Guardian of Data
The Repository Pattern has a simple but vital purpose: Decouple business logic from the origin of data.
The Use Case (or ViewModel) asks: “Give me the users”. The Use Case doesn’t care if the users come from:
- A REST API (Retrofit)
- A local database (Room)
- A JSON file in assets
- An in-memory cache
This allows changing the data implementation without touching a single line of business logic.
🏗️ Anatomy of a Modern Repository
1. The Interface (Domain)
Defines what can be done, not how.
interface ProductRepository {
// Returns Flow for real-time updates
fun getProducts(): Flow<Result<List<Product>>>
// Suspend functions for one-shot operations
suspend fun refreshProducts(): Result<Unit>
suspend fun getProductById(id: String): Result<Product>
}
2. The Implementation (Data Layer)
Here lives the dirty logic of coordination.
class ProductRepositoryImpl @Inject constructor(
private val remote: ProductRemoteDataSource, // Retrofit
private val local: ProductLocalDataSource, // Room
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ProductRepository { ... }
🔄 Synchronization Strategies
The real value of the repository is in how it coordinates Local and Remote.
Strategy: Single Source of Truth (SSOT)
The local database is the ONLY truth.
- UI observes DB (Room Flow).
- When data is requested, Repo launches API call.
- If API responds, Repo saves to DB.
- Room automatically notifies UI with new data.
override fun getProducts(): Flow<Result<List<Product>>> {
return local.getProducts() // Flow from Room
.map { Result.Success(it) }
.onStart {
// Trigger side-effect refresh
try {
val remoteData = remote.fetch()
local.save(remoteData)
} catch (e: Exception) {
emit(Result.Error(e))
}
}
}
This strategy is robust because the app works Offline-First by default.
Strategy: Cache-Aside (Read with Fallback)
Useful for data that changes rarely or isn’t stored in DB.
- Check memory/disk.
- If missing or expired -> Call network.
- Return and save.
⚠️ Common Mistakes
- Exposing DTOs: The Repo must return Domain Models, not
NetworkResponse<UserDto>. Always map inside the Repo. - Business Logic: The Repo shouldn’t decide “if user is VIP, give discount”. That belongs in the Use Case. The Repo only stores and retrieves.
- Threading: The Repo must be “Main-Safe”. Use
withContext(Dispatchers.IO)to ensure calling repo from UI never blocks.
🎯 Conclusion
A good Repository is invisible. The domain layer trusts it blindly. By centralizing data access, you gain the ability to optimize (add in-memory cache, switch from SQL to NoSQL) without breaking the rest of the app. It is the key piece for long-term maintainability.
You might also be interested in
MVVM Model: The Invisible but Vital Data Layer
The 'Model' in MVVM is much more than data classes. Learn to design a robust model layer that survives UI and backend changes.
AI Agents on Android: Theory and Practice
Understanding the role of AI Agents in modern mobile development. From theoretical foundations to practical implementation strategies using LLMs.
Clean Architecture: The Ultimate Guide for Modern Android
Demystifying Clean Architecture: A deep dive into layers, dependencies, and data flow to build indestructible Android apps.