Repository Pattern: La Verdadera Abstracción de Datos
Índice de contenidos
🏛️ Teoría: El Guardián de los Datos
El Repository Pattern tiene un propósito simple pero vital: Desacoplar la lógica de negocio de la procedencia de los datos.
El Use Case (o ViewModel) pregunta: “Dame los usuarios”. Al Use Case no le importa si los usuarios vienen de:
- Una API REST (Retrofit)
- Una base de datos local (Room)
- Un archivo JSON en assets
- Una caché en memoria
Esto permite cambiar la implementación de datos sin tocar ni una línea de la lógica de negocio.
🏗️ Anatomía de un Repositorio Moderno
1. La Interfaz (Dominio)
Define qué se puede hacer, no cómo.
interface ProductRepository {
// Retorna Flow para actualizaciones en tiempo real
fun getProducts(): Flow<Result<List<Product>>>
// Funciones suspendidas para operaciones one-shot
suspend fun refreshProducts(): Result<Unit>
suspend fun getProductById(id: String): Result<Product>
}
2. La Implementación (Capa de Datos)
Aquí vive la lógica sucia de coordinación.
class ProductRepositoryImpl @Inject constructor(
private val remote: ProductRemoteDataSource, // Retrofit
private val local: ProductLocalDataSource, // Room
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ProductRepository { ... }
🔄 Estrategias de Sincronización
El valor real del repositorio está en cómo coordina Local y Remote.
Estrategia: Single Source of Truth (SSOT)
La base de datos local es la ÚNICA verdad.
- La UI observa la DB (Room Flow).
- Cuando se piden datos, el Repo lanza una llamada a la API.
- Si la API responde, el Repo guarda en la DB.
- Room notifica automáticamente a la UI con los nuevos datos.
override fun getProducts(): Flow<Result<List<Product>>> {
return local.getProducts() // Flow desde Room
.map { Result.Success(it) }
.onStart {
// Trigger refresh lateral
try {
val remoteData = remote.fetch()
local.save(remoteData)
} catch (e: Exception) {
emit(Result.Error(e))
}
}
}
Esta estrategia es robusta porque la app funciona Offline-First por defecto.
Estrategia: Cache-Aside (Lectura con Fallback)
Útil para datos que cambian poco o no se guardan en DB.
- Busca en memoria/disco.
- Si no existe o expiró -> Llama a red.
- Retorna y guarda.
⚠️ Errores Comunes
- Exponer DTOs: El Repo debe devolver Modelos de Dominio, no
NetworkResponse<UserDto>. Mapea siempre dentro del Repo. - Lógica de Negocio: El Repo no debe decidir “si el usuario es VIP, dale descuento”. Eso es del Use Case. El Repo solo almacena y recupera.
- Manejo de Hilos: El Repo debe ser “Main-Safe”. Usa
withContext(Dispatchers.IO)para asegurarte de que llamar al repo desde la UI nunca bloquee.
🎯 Conclusión
Un buen Repository es invisible. La capa de dominio confía ciegamente en él. Al centralizar el acceso a datos, ganas la capacidad de optimizar (añadir caché en memoria, cambiar de SQL a NoSQL) sin romper el resto de la app. Es la pieza clave de la mantenibilidad a largo plazo.
Artículos relacionados
MVVM Model: La Capa de Datos Invisible pero Vital
El 'Model' en MVVM es mucho más que clases de datos. Aprende a diseñar una capa de modelo robusta que sobreviva a cambios de UI y backend.
SOLID en Android: Clean Architecture para Apps Modernas
Descubre cómo aplicar los principios SOLID en tu app de puzzles Android para crear una arquitectura robusta, escalable y mantenible.
Clean Architecture: La Guía Definitiva para Android Moderno
Desmitificando Clean Architecture: Una inmersión profunda en capas, dependencias y flujo de datos para construir apps Android indestructibles.