Patrón de repositorio

1. Antes de comenzar

Introducción

En este codelab, mejorarás la experiencia del usuario para una app con almacenamiento en caché sin conexión. Muchas apps dependen de datos de la red. Si tu app recupera datos del servidor en cada inicio y el usuario ve una pantalla de carga, es posible que una mala experiencia del usuario los lleve a desinstalar tu app.

Cuando los usuarios inician una app, esperan que esta muestre datos con rapidez. Para lograr este objetivo, implementa el almacenamiento en caché sin conexión. Esto significa que tu app guardará los datos obtenidos de la red en el almacenamiento local del dispositivo, lo que dará como resultado un acceso más rápido.

Debido a que la app podrá obtener datos de la red y guardar una caché sin conexión de los resultados descargados anteriormente, necesitarás una manera de organizar todas estas fuentes de datos. Para ello, implementarás una clase de repositorio, que servirá como única fuente de verdad para los datos de la app y abstraerás la fuente de los datos (red, caché, etc.) fuera del modelo de vista.

Conocimientos que ya deberías tener

Debes estar familiarizado con lo siguiente:

Qué aprenderás

  • Cómo implementar un repositorio para abstraer la capa de datos de una app a partir del resto de la app
  • Cómo cargar datos almacenados en caché mediante un repositorio

Actividades

  • Usar un repositorio para abstraer la capa de datos y, luego, integrar la clase del repositorio con el ViewModel
  • Mostrar datos de la caché sin conexión

2. Código de inicio

Descarga el código del proyecto

Ten en cuenta que el nombre de la carpeta es RepositoryPattern-Starter. Selecciona esta carpeta cuando abras el proyecto en Android Studio.

A fin de obtener el código necesario para este codelab y abrirlo en Android Studio, haz lo siguiente:

Obtén el código

  1. Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
  2. Verifica y confirma que el nombre de la rama coincida con el nombre de la rama que se especifica en el codelab. Por ejemplo, en la siguiente captura de pantalla, el nombre de la rama es main.

8cf29fa81a862adb.png

  1. En la página de GitHub del proyecto, haz clic en el botón Code, que abre una ventana emergente.

1debcf330fd04c7b.png

  1. En la ventana emergente, haz clic en el botón Download ZIP para guardar el proyecto en tu computadora. Espera a que se complete la descarga.
  2. Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
  3. Haz doble clic en el archivo ZIP para descomprimirlo. Se creará una carpeta nueva con los archivos del proyecto.

Abre el proyecto en Android Studio

  1. Inicia Android Studio.
  2. En la ventana Welcome to Android Studio, haz clic en Open.

d8e9dbdeafe9038a.png

Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > Open.

8d1fda7396afe8e5.png

  1. En el navegador de archivos, ve hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
  2. Haz doble clic en la carpeta del proyecto.
  3. Espera a que Android Studio abra el proyecto.
  4. Haz clic en el botón Run 8de56cba7583251f.png para compilar y ejecutar la app. Asegúrate de que funcione como se espera.

3. Descripción general de la app de inicio

La app de DevBytes presenta una lista de videos de DevBytes del canal de YouTube para desarrolladores de Android en una vista de reciclador, en la que los usuarios pueden hacer clic a fin de abrir un vínculo al video.

9757e53b89d2de7c.png

Si bien el código de inicio funciona por completo, tiene una falla importante que puede afectar negativamente la experiencia del usuario. Si el usuario tiene una conexión irregular o no tiene ninguna conexión a Internet, no se mostrará ninguno de los videos. Esto ocurre incluso si la app se abrió con anterioridad. Si el usuario sale y reinicia la app de nuevo, pero esta vez sin Internet, la app intentará volver a descargar la lista de videos y no podrá hacerlo.

Puedes ver esto en acción en el emulador.

  1. Activa temporalmente el modo de avión en el emulador de Android (App de Configuración > Internet y redes > Modo de avión).
  2. Ejecuta la app de DevBytes y observa que la pantalla está en blanco.

f0365b27d0dd8f78.png

  1. Asegúrate de desactivar el modo de avión antes de continuar con el resto del codelab.

Esto se debe a que, después de que la app de DevBytes descarga los datos por primera vez, no se almacena nada en caché para un uso posterior. Actualmente, la app incluye una base de datos de Room. Tu tarea es usarla con el fin de implementar la funcionalidad de almacenamiento en caché y actualizar el modelo de vista a los efectos de usar un repositorio, que descargará datos nuevos o los recuperará de la base de datos de Room. La clase de repositorio abstrae esta lógica del modelo de vista, lo que mantiene tu código organizado y separado.

El proyecto inicial se organiza en varios paquetes.

25b5f8d0997df54c.png

Si bien te invitamos a familiarizarte con el código, solo trabajarás sobre dos archivos: repository/VideosRepository.kt y viewmodels/DevByteViewModel. Primero, crearás una clase VideosRepository que implemente el patrón de repositorio para almacenar en caché (obtendrás más información al respecto en las próximas páginas) y, luego, actualizarás el DevByteViewModel a fin de usar tu nueva clase VideosRepository.

Sin embargo, antes de comenzar directamente con el código, dediquemos un momento a obtener más información sobre el almacenamiento en caché y el patrón de repositorio.

4. Almacenamiento en caché y patrón de repositorio

Repositorios

El patrón de repositorio es un patrón de diseño que aísla la capa de datos del resto de la app. La capa de datos hace referencia a la parte de tu app, independiente de la IU, que controla los datos y la lógica empresarial de la app, lo que expone API coherentes de modo que el resto de la app acceda a esos datos. Mientras que la IU presenta información al usuario, la capa de datos incluye elementos como el código de red, las bases de datos de Room, el manejo de errores y cualquier código que lea o manipule datos.

9e528301efd49aea.png

Un repositorio puede resolver conflictos entre fuentes de datos (como modelos persistentes, servicios web y cachés) y centralizar los cambios en estos datos. En el siguiente diagrama, se muestra cómo los componentes de la app, como las actividades, podrían interactuar con las fuentes de datos mediante un repositorio.

69021c8142d29198.png

Para implementar un repositorio, usa una clase separada, como la clase VideosRepository, que crearás en la siguiente tarea. La clase de repositorio aísla las fuentes de datos del resto de la app y proporciona una API limpia para el acceso de datos al resto de la app. Usar una clase de repositorio garantiza que este código sea independiente de la clase ViewModel y es una práctica recomendada para la separación del código y su arquitectura.

Ventajas de usar un repositorio

Un módulo de repositorio controla operaciones de datos y te permite usar varios backends. En una app real típica, el repositorio implementa la lógica para decidir si debe recuperar datos de una red o usar resultados almacenados en caché de una base de datos local. Con un repositorio, puedes intercambiar los detalles de la implementación, como la migración a una biblioteca de persistencia diferente, sin afectar el código de llamada, como los modelos de vista. Esto también permite que tu código sea modular y se pueda probar. Puedes simular con facilidad el repositorio y probar el resto del código.

Un repositorio debe funcionar como una única fuente de verdad para una parte específica de los datos de tu app. Cuando se trabaja con varias fuentes de datos, como un recurso conectado en red y una caché sin conexión, el repositorio garantiza que los datos de la app sean lo más precisos y actualizados, lo que proporcionará la mejor experiencia posible incluso cuando la app esté sin conexión.

Almacenamiento en caché

Una caché es un almacenamiento de datos que usa la app. Por ejemplo, puedes guardar datos de la red de forma temporal en caso de que se interrumpa la conexión a Internet del usuario. Si bien la red ya no está disponible, la app puede recurrir a los datos almacenados en caché. Un almacenamiento en caché también puede ser útil para almacenar datos temporales de una actividad que ya no está en pantalla o, incluso, datos persistentes entre cada inicio de la app.

Una caché puede adoptar muchas formas, algunas más simples o más complejas, en función de la tarea específica. En la siguiente tabla, se muestran varias formas de implementar el almacenamiento en caché en la red para Android.

Técnica de almacenamiento en caché

Usos

Retrofit es una biblioteca de herramientas de redes que se usa a fin de implementar un cliente de REST de tipo seguro para Android. Puedes configurar Retrofit de modo que almacene una copia de cada resultado de red de manera local.

Es una buena solución para respuestas y solicitudes simples, llamadas de red poco frecuentes o conjuntos de datos pequeños.

Puedes usar DataStore para almacenar pares clave-valor.

Es una buena solución para una pequeña cantidad de claves y valores simples, como la configuración de la app. No puedes usar esta técnica a los efectos de almacenar grandes cantidades de datos estructurados.

Puedes acceder al directorio de almacenamiento interno de la app y guardar los archivos de datos allí. El nombre del paquete de tu app especifica el directorio de almacenamiento interno de la app, que se encuentra en una ubicación especial del sistema de archivos de Android. Este directorio es privado para tu app y se borra cuando esta se desinstala.

Resulta una buena solución si tienes necesidades específicas que un sistema de archivos puede resolver, por ejemplo, si necesitas guardar archivos multimedia o de datos, y debes administrarlos por tu cuenta. No puedes usar esta técnica para almacenar datos estructurados y complejos que tu app necesite consultar.

Puedes almacenar datos en caché con Room, una biblioteca de asignación de objetos de SQLite que proporciona una capa de abstracción sobre SQLite.

Se trata de la solución recomendada para datos estructurados complejos que se pueden consultar, ya que la mejor manera de almacenar datos estructurados en el sistema de archivos de un dispositivo es mediante una base de datos SQLite local.

En este codelab, usarás Room, ya que es la manera recomendada de almacenar datos estructurados en el sistema de archivos de un dispositivo. La app de DevBytes ya está configurada para usar Room. Tu tarea es implementar el almacenamiento en caché sin conexión mediante el patrón de repositorio a fin de separar la capa de datos del código de la IU.

5. Cómo implementar VideosRepository

Tarea: Crea un repositorio

En esta tarea, crearás un repositorio para administrar la caché sin conexión, que implementaste en la tarea anterior. Tu base de datos de Room no tiene lógica para administrar la caché sin conexión, solo cuenta con métodos para insertar, actualizar, borrar y recuperar datos. El repositorio tendrá la lógica necesaria para recuperar los resultados de red y mantener la base de datos actualizada.

Paso 1: Agrega un repositorio

  1. En repository/VideosRepository.kt, crea una clase VideosRepository. Pasa un objeto VideosDatabase como el parámetro constructor de la clase para acceder a los métodos DAO.
class VideosRepository(private val database: VideosDatabase) {
}
  1. Dentro de la clase VideosRepository, agrega un método suspend llamado refreshVideos() que no tenga argumentos y no muestre nada. Este método será la API que se usa para actualizar la caché sin conexión.
suspend fun refreshVideos() {
}
  1. Dentro del método refreshVideos(), cambia el contexto de las corrutinas a Dispatchers.IO con el fin de realizar operaciones de red y de base de datos.
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
   }
}
  1. Dentro del bloque withContext, recupera la lista de reproducción de videos de DevByte desde la red mediante la instancia de servicio de Retrofit, DevByteNetwork.
val playlist = DevByteNetwork.devbytes.getPlaylist()
  1. Dentro del método refreshVideos(), después de recuperar la lista de reproducción de la red, almacena esa lista en la base de datos de Room. Para almacenar la lista de reproducción, usa la clase VideosDatabase. Llama al método DAO insertAll() y pasa la lista de reproducción recuperada de la red. Usa la función de extensión asDatabaseModel() para asignar la lista de reproducción al objeto de base de datos.
database.videoDao.insertAll(playlist.asDatabaseModel())
  1. Aquí se muestra el método refreshVideos() completo con una instrucción de registro para realizar un seguimiento cuando se lo llama:
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
       val playlist = DevByteNetwork.devbytes.getPlaylist()
       database.videoDao.insertAll(playlist.asDatabaseModel())
   }
}

Paso 2: Recupera datos de la base de datos

En este paso, crearás un objeto LiveData para leer la lista de reproducción de videos desde la base de datos. Este objeto LiveData se actualiza automáticamente cuando se actualiza la base de datos. El fragmento adjunto, o la actividad, se actualiza con valores nuevos.

  1. En la clase VideosRepository, declara un objeto LiveData llamado videos para incluir una lista de objetos DevByteVideo. Inicializa el objeto videos con database.videoDao. Llama al método DAO getVideos(). Como el método getVideos() muestra una lista de objetos de base de datos y no una lista de objetos DevByteVideo, Android Studio muestra un error de "tipo no coincidente".
val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos()
  1. A los efectos de corregir el error, usa Transformations.map y convierte la lista de objetos de base de datos en una lista de objetos de dominio mediante la función de conversión asDomainModel().
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
   it.asDomainModel()
}

Ya implementaste un repositorio para la app. En la próxima tarea, usarás una estrategia de actualización simple para mantener la base de datos local actualizada.

6. Cómo usar VideosRepository en DevByteViewModel

Tarea: Integra el repositorio mediante una estrategia de actualización

En esta tarea, integrarás el repositorio con el ViewModel mediante una estrategia de actualización simple. Mostrarás la lista de reproducción de video de la base de datos de Room, sin recuperar datos de forma directa desde la red.

La actualización de una base de datos es un proceso de actualización de la base de datos local con el fin de mantenerla sincronizada con datos de la red. Para esta app de ejemplo, usarás una estrategia de actualización simple, en la que el módulo que solicita datos del repositorio se encarga de actualizar los datos locales.

En una app real, la estrategia podría ser más compleja. Por ejemplo, tu código podría actualizar automáticamente los datos en segundo plano (teniendo en cuenta el ancho de banda) o almacenar en caché los datos que el usuario tiene más probabilidades de usar a continuación.

  1. En viewmodels/DevByteViewModel.kt, dentro de la clase DevByteViewModel, crea una variable de miembro privada llamada videosRepository del tipo VideosRepository. Para crear una instancia de la variable, pasa el objeto singleton VideosDatabase.
private val videosRepository = VideosRepository(getDatabase(application))
  1. En la clase DevByteViewModel, reemplaza el método refreshDataFromNetwork() por el método refreshDataFromRepository(). El método antiguo, refreshDataFromNetwork(), recuperó la lista de reproducción de videos de la red mediante la biblioteca Retrofit. Con el nuevo método, se carga la lista de reproducción de videos desde el repositorio. El repositorio determina desde qué fuente (p. ej., la red, la base de datos, etc.) se recuperará la lista de reproducción y mantiene los detalles de la implementación fuera del modelo de vista. El repositorio también hace que tu código resulte más fácil de mantener: si cambiaras la implementación para obtener los datos en el futuro, no tendrías que modificar el modelo de vista.
private fun refreshDataFromRepository() {
   viewModelScope.launch {
       try {
           videosRepository.refreshVideos()
           _eventNetworkError.value = false
           _isNetworkErrorShown.value = false

       } catch (networkError: IOException) {
           // Show a Toast error message and hide the progress bar.
           if(playlist.value.isNullOrEmpty())
               _eventNetworkError.value = true
       }
   }
}
  1. En la clase DevByteViewModel, dentro del bloque init, cambia la llamada a función de refreshDataFromNetwork() a refreshDataFromRepository(). Este código recupera la lista de reproducción de videos del repositorio, no lo hace directamente desde la red.
init {
   refreshDataFromRepository()
}
  1. En la clase DevByteViewModel, borra la propiedad _playlist y su propiedad de copia de seguridad, playlist.

Código que debes borrar

private val _playlist = MutableLiveData<List<Video>>()
...
val playlist: LiveData<List<Video>>
   get() = _playlist
  1. En la clase DevByteViewModel, después de crear una instancia del objeto videosRepository, agrega una nueva val llamada playlist a fin de que contenga una lista LiveData de videos desde el repositorio.
val playlist = videosRepository.videos
  1. Ejecuta la app. La app se ejecutará como antes, pero ahora la lista de reproducción DevBytes se recupera desde la red y se guarda en la base de datos de Room. Se mostrará la lista de reproducción en la pantalla desde la base de datos de Room, no directamente desde la red.

30ee74d946a2f6ca.png

  1. Para notar la diferencia, habilita el modo de avión en el emulador o dispositivo.
  2. Vuelve a ejecutar la app. Observa que no se muestra el mensaje de aviso de "Error de red". En su lugar, se muestra la lista de reproducción recuperada de la caché sin conexión.
  3. Desactiva el modo de avión en el emulador o dispositivo.
  4. Cierra la app y vuelve a abrirla. La app cargará la lista de reproducción desde la caché sin conexión, mientras se ejecuta la solicitud de red en segundo plano.

Si entraran datos nuevos desde la red, la pantalla se actualizará automáticamente y los mostrará. Sin embargo, el servidor DevBytes no actualizará su contenido, por lo que no verás la actualización de los datos.

¡Muy bien! En este codelab, integraste una caché sin conexión con un ViewModel a fin de mostrar la lista de reproducción desde el repositorio en lugar de recuperarla desde la red.

7. Código de solución

Código de solución

Proyecto de Android Studio: RepositoryPattern

8. Felicitaciones

¡Felicitaciones! En esta ruta de aprendizaje, aprendiste lo siguiente:

  • El almacenamiento en caché es un proceso de almacenamiento de datos recuperados desde una red en el almacenamiento de un dispositivo. Este proceso permite que tu app acceda a datos cuando el dispositivo no tiene conexión o cuando esta necesite acceder a los mismos datos nuevamente.
  • La mejor manera de que tu app almacene datos estructurados en el sistema de archivos de un dispositivo es usar una base de datos local SQLite. Room es una biblioteca de asignación de objetos de SQLite, lo que significa que proporciona una capa de abstracción para SQLite. Te recomendamos que uses Room a fin de implementar el almacenamiento en caché sin conexión.
  • Una clase de repositorio aísla las fuentes de datos, como una base de datos de Room y servicios web, del resto de la app. Esa clase proporciona una API limpia para el acceso de datos al resto de la app.
  • Usar repositorios es una práctica recomendada para la separación del código y su arquitectura.
  • Cuando diseñas una caché sin conexión, te recomendamos que separes la red, el dominio y los objetos de la base de datos de la app. Esta estrategia es un ejemplo de la separación de inquietudes.

Más información