Implementación de Repositorio para Preórdenes en Android Studio

Clase 11 de 19Curso de Android: Modo Offline con Room y Realm

Resumen

La implementación de repositorios en aplicaciones Android es fundamental para mantener una arquitectura limpia y escalable. Al separar la lógica de acceso a datos, tanto locales como remotos, conseguimos un código más mantenible y testeable. En esta ocasión, completaremos la capa de datos con la implementación del repositorio para preórdenes, un componente esencial que nos permitirá gestionar eficientemente la información antes de confirmar pedidos.

¿Cómo implementar un repositorio para preórdenes en Android?

Cuando desarrollamos aplicaciones que manejan datos, es crucial establecer una estructura que permita la comunicación fluida entre las diferentes capas de la arquitectura. El repositorio actúa como intermediario entre las fuentes de datos y la lógica de negocio, proporcionando una API limpia para el resto de la aplicación.

Para implementar nuestro repositorio de preórdenes, comenzamos creando una nueva clase en la carpeta de data:

class PreOrderRepositoryImpl(
    private val remoteDataStorage: RemoteDataStorage,
    private val localStorage: LocalStorage
) : PreOrderRepository {
    // Implementación de métodos
}

Esta clase implementa la interfaz PreOrderRepository y recibe dos dependencias fundamentales:

  • RemoteDataStorage: Gestiona la comunicación con servicios externos
  • LocalStorage: Maneja el almacenamiento local de datos

¿Qué métodos debe implementar nuestro repositorio de preórdenes?

El repositorio debe proporcionar métodos para realizar operaciones CRUD (Crear, Leer, Actualizar, Eliminar) sobre las preórdenes. Veamos la implementación de cada uno:

Guardar una preorden

override fun savePreOrder(preOrder: PreOrder): Flow<Result<Boolean>> = flow {
    try {
        val resolve = remoteDataStorage.savePreOrder(preOrder)
        localStorage.savePreOrderInRoom(
            PreOrderEntity(
                preOrderId = preOrder.customerId,
                customerName = preOrder.customerName,
                product = preOrder.product,
                synced = resolve
            )
        )
        emit(Result.success(resolve))
    } catch (e: Exception) {
        emit(Result.failure(e))
    }
}

Este método realiza dos operaciones principales:

  1. Intenta sincronizar la preorden con el servidor remoto
  2. Almacena la preorden localmente con un flag que indica si la sincronización fue exitosa

Es importante notar que utilizamos Flow para manejar operaciones asíncronas, lo que permite a la capa de presentación reaccionar a los cambios en los datos.

Obtener preórdenes almacenadas localmente

override fun getLocalPreOrders(): Flow<List<PreOrder>> = flow {
    try {
        val preOrders = localStorage.getPreOrdersFromRoom()
            .map { it.toDomain() }
        emit(preOrders)
    } catch (e: Exception) {
        emit(emptyList())
    }
}

Este método:

  1. Recupera las preórdenes almacenadas en la base de datos local
  2. Transforma cada entidad de base de datos a un objeto de dominio mediante el método toDomain()
  3. Emite la lista de preórdenes al colector del flujo

¿Cómo convertir entidades de base de datos a objetos de dominio?

Para mantener la separación de capas, necesitamos mapear las entidades de la base de datos a objetos del dominio:

fun PreOrderEntity.toDomain(): PreOrder {
    return PreOrder(
        id = preOrderId,
        customerName = customerName,
        product = product,
        synced = synced
    )
}

Esta función de extensión nos permite convertir fácilmente entre los diferentes modelos de datos utilizados en las distintas capas de la aplicación.

Eliminación y reintento de sincronización

override fun deletePreOrder(id: String) {
    localStorage.deletePreOrderInRoom(id)
}

override fun retrySync(preOrder: PreOrder): Flow<Result<Boolean>> = flow {
    try {
        val resolve = remoteDataStorage.savePreOrder(preOrder)
        if (resolve) {
            localStorage.updatePreOrderInRoom(preOrder.id, true)
        }
        emit(Result.success(resolve))
    } catch (e: Exception) {
        emit(Result.failure(e))
    }
}

El método deletePreOrder simplemente elimina una preorden de la base de datos local, mientras que retrySync intenta sincronizar nuevamente una preorden con el servidor remoto y actualiza su estado en la base de datos local si la operación es exitosa.

¿Cómo integrar el repositorio en la arquitectura con Hilt?

Una vez implementado el repositorio, necesitamos integrarlo en el gráfico de dependencias de nuestra aplicación. Para ello, utilizamos Hilt, un framework de inyección de dependencias para Android:

@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {

    @Provides
    @Singleton
    fun provideOrdersRepository(
        remoteDataStorage: RemoteDataStorage,
        localStorage: LocalStorage
    ): OrderRepository {
        return OrderRepositoryImpl(remoteDataStorage, localStorage)
    }

    @Provides
    @Singleton
    fun providePreOrderRepository(
        remoteDataStorage: RemoteDataStorage,
        localStorage: LocalStorage
    ): PreOrderRepository {
        return PreOrderRepositoryImpl(remoteDataStorage, localStorage)
    }
}

Este módulo de Hilt define cómo se deben crear las instancias de nuestros repositorios:

  1. La anotación @Module indica que esta clase proporciona dependencias
  2. @InstallIn(SingletonComponent::class) especifica que las dependencias estarán disponibles durante toda la vida de la aplicación
  3. @Provides y @Singleton indican que se proporcionará una única instancia de cada repositorio

Es importante destacar que Hilt se encarga automáticamente de proporcionar las instancias de RemoteDataStorage y LocalStorage gracias a la anotación @Inject que deben tener estas clases.

Con la implementación de este repositorio, completamos la capa de datos de nuestra aplicación. Ahora tenemos una estructura sólida que separa claramente las responsabilidades y facilita la comunicación entre las diferentes partes de nuestra arquitectura.

¿Has implementado repositorios en tus proyectos Android? ¿Qué patrones has encontrado más útiles para gestionar la sincronización entre datos locales y remotos? Comparte tu experiencia en los comentarios.