Implementación de Repositorios en Android: Remote y Local Storage
Clase 10 de 19 • Curso de Android: Modo Offline con Room y Realm
Resumen
La implementación de repositorios es un componente fundamental en el desarrollo de aplicaciones Android modernas. A través de este patrón, podemos gestionar eficientemente el acceso a datos tanto remotos como locales, creando una capa de abstracción que facilita el mantenimiento y la escalabilidad de nuestras aplicaciones. El manejo adecuado de los datos es crucial para ofrecer una experiencia de usuario fluida, especialmente cuando necesitamos sincronizar información entre una API remota y una base de datos local.
¿Cómo implementar un repositorio para gestionar órdenes en Android?
Para implementar un repositorio que gestione órdenes en nuestra aplicación Android, necesitamos crear una clase que extienda de nuestra interfaz de repositorio. Esta clase será responsable de coordinar la comunicación entre nuestras fuentes de datos remotas (API) y locales (base de datos Room).
class OrderRepositoryImpl(
private val remoteDataStorage: RemoteDataStorage,
private val localStorage: LocalStorage
) : OrderRepository {
override suspend fun getOrders(): Result<List<Order>> {
// Primero obtenemos datos locales
return localStorage.getOrdersRun().map { localOrders ->
// Mapeamos entidades locales a dominio
localOrders.map { it.toDomain() }
}.also {
// Luego disparamos petición remota
try {
val remoteResponse = remoteDataStorage.getOrders()
remoteResponse?.let { orders ->
// Guardamos respuesta remota localmente
localStorage.saveOrdersRun(orders.map { it.toDomain().toEntity() })
}
} catch (e: Exception) {
// Manejo de excepciones
}
}
}
override suspend fun getOrderById(id: String): Result<Order> {
return localStorage.getOrderByIdRun(id).runCatching {
this.toDomain()
}
}
}
En este código, primero obtenemos los datos almacenados localmente, los mapeamos al modelo de dominio y luego, de manera asíncrona, actualizamos la base de datos local con los datos más recientes de la API.
¿Por qué necesitamos mapear entre diferentes modelos de datos?
El mapeo entre diferentes modelos de datos es esencial cuando trabajamos con arquitecturas limpias o en capas. Cada capa de nuestra aplicación maneja su propio modelo de datos:
- Capa de datos: Utiliza entidades específicas para Room (LocalStorage) y DTOs para la API (RemoteDataStorage)
- Capa de dominio: Utiliza modelos de negocio independientes de la implementación
Para facilitar este mapeo, creamos funciones de extensión que convierten entre los diferentes tipos:
// Mapeo de entidad Room a modelo de dominio
fun OrderEntity.toDomain(): Order {
return Order(
id = id,
customerName = customerName,
itemTotal = itemTotal,
imageUrl = imageUrl
)
}
// Mapeo de DTO a modelo de dominio
fun OrderDTO.toDomain(): Order {
return Order(
id = id,
customerName = customerName,
itemTotal = itemTotal,
imageUrl = imageUrl
)
}
// Mapeo de modelo de dominio a entidad Room
fun Order.toEntity(): OrderEntity {
return OrderEntity(
id = id,
customerName = customerName,
itemTotal = itemTotal,
imageUrl = imageUrl
)
}
Estas funciones de extensión nos permiten convertir fácilmente entre los diferentes modelos, manteniendo nuestro código limpio y organizado.
¿Cómo manejar errores en los repositorios?
El manejo de errores es una parte crucial de cualquier aplicación robusta. En nuestra implementación, utilizamos el tipo Result
para encapsular tanto los resultados exitosos como los errores:
override suspend fun getOrderById(id: String): Result<Order> {
return localStorage.getOrderByIdRun(id).runCatching {
this.toDomain()
}
}
La función runCatching
nos permite capturar cualquier excepción que pueda ocurrir durante la ejecución y convertirla en un Result.Failure
, mientras que un resultado exitoso se convierte en un Result.Success
.
¿Cómo sincronizar datos remotos con la base de datos local?
La sincronización entre datos remotos y locales sigue un patrón común:
- Primero devolvemos los datos locales para una respuesta rápida al usuario
- Luego solicitamos datos actualizados de la API remota
- Finalmente actualizamos la base de datos local con los nuevos datos
// Primero obtenemos datos locales
return localStorage.getOrdersRun().map { localOrders ->
// Mapeamos entidades locales a dominio
localOrders.map { it.toDomain() }
}.also {
// Luego disparamos petición remota
try {
val remoteResponse = remoteDataStorage.getOrders()
remoteResponse?.let { orders ->
// Guardamos respuesta remota localmente
localStorage.saveOrdersRun(orders.map { it.toDomain().toEntity() })
}
} catch (e: Exception) {
// Manejo de excepciones
}
}
Este enfoque, conocido como Single Source of Truth, garantiza que nuestra aplicación siempre muestre datos consistentes, incluso cuando no hay conexión a internet.
La implementación de repositorios es una práctica fundamental para crear aplicaciones Android robustas y mantenibles. Al separar la lógica de acceso a datos de la lógica de negocio, creamos un código más limpio y fácil de probar. ¿Has implementado repositorios en tus proyectos? Comparte tu experiencia en los comentarios.