Last Updated: 2021-28-07

¿Qué construirá?

En este tutorial se trabajará sobre una aplicación desarrollada para un tutorial anterior. Dicha aplicación permite visualizar la información de un API REST que contiene información acerca de coleccionistas de álbumes musicales y los comentarios que realizan sobre ellos.

Al final de este tutorial usted tendrá:

¿Qué aprenderá?

Al desarrollar este tutorial aprenderá:

¿Qué necesita?

Como se mencionó en la sección introductoria, es necesario que, para este tutorial haya desarrollado previamente el tutorial "Cómo desarrollar aplicaciones para Android con las librerías de Jetpack".

Como el énfasis de este tutorial está únicamente en los elementos de arquitectura que permiten utilizar los patrones Repository y Service Adapter, no se harán modificaciones sobre el aspecto de la aplicación, ni los elementos gráficos, ni los flujos de navegación. Por ende, la aplicación desarrollada en el tutorial anterior es perfecta como base para realizar las modificaciones que implica la inclusión de los patrones mencionados. Dicho lo anterior, vale la pena recordar los detalles de la aplicación que se desarrolló en el pasado tutorial para tener claros los pasos a seguir en el presente.

Obtener el código fuente de la aplicación

En este punto, es ideal que parta del código fuente de la aplicación que usted desarrolló en el tutorial mencionado anteriormente. Por este motivo, el proyecto inicial de este tutorial es el resultado final al que llegó al momento de realizar el tutorial. Sin embargo, en este apartado se brindarán las instrucciones para obtener el código fuente del proyecto desarrollado por los tutores del curso, en caso de que no cuente con el proyecto del tutorial anterior. Es posible que también ya haya descargado el repositorio de los tutoriales del curso previamente, en cuyo caso puede omitir este apartado.

El código de este proyecto se encuentra alojado en un repositorio de GitHub junto con los demás tutoriales. Este repositorio está disponible en el siguiente enlace: https://github.com/TheSoftwareDesignLab/MISW4104-Ejemplos.git. Ingrese allí para ver el código del proyecto. Como con todos los proyectos de GitHub, usted puede descargar los archivos del repositorio a su máquina local por varios medios.

Si lo desea, puede optar por descargar el código fuente directamente en forma de archivo .zip desde la página web del repositorio en GitHub, haciendo clic en el botón "Download Zip" del desplegable Code, el cual se ve en la siguiente imagen:

Panel desplegable del sitio web del repositorio para descargar el código

Imagen 1. Vista desplegable del botón Code

No obstante, si cuenta con GitHub Desktop, puede hacer clic en el botón Open with GitHub Desktop. Si cuenta con Git instalado localmente en su máquina, puede clonar el repositorio a un directorio local vacío por medio del comando git clone https://github.com/TheSoftwareDesignLab/MISW4104-Ejemplos.git o por medio de la interfaz gráfica de Git. Finalmente, también puede optar por descargar el proyecto directamente con Android Studio haciendo uso de la opción Get from Version Control del menú inicial de Android Studio, la cual se muestra en la siguiente imagen:

Al abrir Android Studio sin un proyecto se muestra un menú con varios botones para abrir o importar un proyecto, perfilar un APK o abrir configuraciones del IDE

Imagen 2. Menú de inicio de Android Studio

Ejecutar la aplicación en su dispositivo

Una vez tenga el proyecto abierto en Android Studio y un dispositivo físico o virtual emparejado, ejecute la aplicación con el botón Run de la esquina superior derecha del IDE. Luego de que los comandos de Gradle terminen, se instalará la aplicación en su dispositivo. Al inicio podrá ver una pantalla como la siguiente, donde se muestra un listado de coleccionistas:

Una vista del fragmento de coleccionistas.  Contiene un RecyclerView con dos coleccionistas con los nombres Manolo Bellon y Jaime Monsalve

Imagen 3. Vista del listado de coleccionistas

Seleccione uno de los coleccionistas y podrá ver posteriormente un listado con los álbumes en el sistema, en una pantalla como la siguiente:

Vista del fragmento de álbumes. Contiene un RecyclerView con el título y la descripción de los álbumes Buscando América, Poeta del pueblo, A night at the opera y A day at the races

Imagen 4. Vista del listado de álbumes

Seleccione un álbum y podrá ver ahora una nueva pantalla con los comentarios que se han hecho para dicho álbum, como se muestra a continuación:

Vista del fragmento de comentarios. Contiene un Recycler View con un único comentario de rating 5 sobre el álbum Buscando América

Imagen 5. Vista de los comentarios de un álbum

Como pudo observar, esta es una aplicación bastante sencilla que incluye un flujo de navegación entre vistas y la obtención de información desde un servicio web a partir de consultas HTTP.

Ahora que interactuó con la interfaz gráfica de la aplicación, es importante que observe y preste atención a ciertos elementos del código fuente que le permitirán entender el funcionamiento de la aplicación y la estructuración del proyecto en una arquitectura de Jetpack. Sin embargo, varios de estos elementos serán omitidos en esta sección, puesto que han sido explicados en detalle en los tutoriales anteriores (como, por ejemplo, las actividades, los fragmentos, o los expuestos en el tutorial sobre Jetpack) donde esta aplicación se ha venido desarrollando.

En particular en este paso es importante concentrarse en el directorio network del código fuente del proyecto, pues allí se encuentra un elemento muy importante llamado NetworkServiceAdapter.

¿Qué es un Service Adapter?

Existe un patrón de diseño llamado adaptador, el cual se utiliza para adaptar o envolver (en un wrapper) una interfaz existente, de forma que pueda ser utilizada como otra interfaz en sí misma por otros miembros, sin que estos tengan interacción directa con la primera interfaz.

Un Service Adapter es, entonces, un elemento de la arquitectura que permite abstraer las funcionalidades relacionadas con un servicio particular al envolver todas las interacciones con una interfaz y exponer a los demás componentes, métodos que puedan ser reutilizados.

Estos pueden ser utilizados en una aplicación de Android para envolver las operaciones de red, de acceso a multimedia y archivos locales, interacción con almacenamiento local, obtención de información por medio de bluetooth, sensores de luz, movimiento, entre otros.

Explorar la clase NetworkServiceAdapter

Ahora bien, en Android Studio abra el archivo NetworkServiceAdapter.kt, que se encuentra en el directorio network de su proyecto. Allí podrá observar varias cosas, de entre las cuales resalta en primera instancia, un companion object que contiene el siguiente código:

companion object{
   const val BASE_URL= "https://vinyl-miso.herokuapp.com/"
   var instance: NetworkServiceAdapter? = null
   fun getInstance(context: Context) =
           instance ?: synchronized(this) {
               instance ?: NetworkServiceAdapter(context).also {
                   instance = it
               }
           }
}

Allí se declaran los elementos estáticos de la clase, lo cual incluye una constante con la URL del servicio web que expone el API REST donde se harán las consultas; una variable del mismo tipo de la clase, y una función para obtener la instancia. Estos elementos son los que permiten implementar un patrón Singleton para este Service Adapter, ya que es ideal que exista una única instancia para toda la aplicación, de forma que no existan duplicados ni memory leaks.

Luego, en el cuerpo de la clase, se puede ver una única propiedad privada de tipo RequestQueue, instanciada en el siguiente código:

private val requestQueue: RequestQueue by lazy {
   // applicationContext keeps you from leaking the Activity or BroadcastReceiver if someone passes one in.
   Volley.newRequestQueue(context.applicationContext)
}

Este es un elemento fundamental de la librería Volley, cuyo uso se especificó detalladamente en el tutorial "Cómo invocar APIs Rest desde aplicaciones nativas por medio de Frameworks". Esta cola se instancia con el contexto de la aplicación de forma que pueda ser compartida por varias vistas, y es creada con estrategia lazy, de forma que solo es instanciada hasta el momento en que se necesite por primera vez.

Finalmente, y como característica más importante del Service Adapter, se pueden ver varias funciones, de las cuales algunas son privadas. Note que las funciones privadas getRequest, postRequest y putRequest son de uso genérico de la librería Volley para crear un objeto de tipo StringRequest o JsonObjectRequest según la necesidad. Por otra parte, las funciones sin modificador de privacidad getAlbums, getCollectors, getComments y postComment son funciones más especializadas, lo cual es posible observar desde el mismo nombre de la función, ya que se especifican los datos sobre los cuales van a realizarse consultas de red. Estos métodos serán expuestos a los demás componentes de la aplicación y, contienen internamente un llamado a la cola de Volley, donde se agregan las peticiones HTTP particulares del API REST y se retorna la información solicitada a los demás componentes (en este caso, por medio de callbacks). A continuación se muestra el código del método getAlbums para ejemplificar su uso:

fun getAlbums( onComplete:(resp:List<Album>)->Unit , onError: (error:VolleyError)->Unit){
   requestQueue.add(getRequest("albums",
           Response.Listener<String> { response ->
               val resp = JSONArray(response)
               val list = mutableListOf<Album>()
               for (i in 0 until resp.length()) {
                   val item = resp.getJSONObject(i)
                   list.add(i, Album(albumId = item.getInt("id"),name = item.getString("name"), cover = item.getString("cover"), recordLabel = item.getString("recordLabel"), releaseDate = item.getString("releaseDate"), genre = item.getString("genre"), description = item.getString("description")))
               }
               onComplete(list)
           },
           Response.ErrorListener {
               onError(it)
           }))
}

En primer lugar, es posible ver que este método se encarga de manejar el requestQueue y agregar el Request que genera el método privado getRequest, mencionado anteriormente. Así mismo, este método se encarga de entregar una respuesta a modo de callback de una lista de elementos de tipo Album luego de modificar su formato original, que era un String enviado a través de internet.

Para implementar el patrón repository en una aplicación desarrollada en una arquitectura de Jetpack, como la que se está utilizando en este tutorial, es necesario definir un componente llamado repositorio para cada una de las vistas de la aplicación. Vale la pena recordar, antes de ello, el concepto de repositorio, para comprender mejor el proceso de implementación del mismo.

¿Qué es un repositorio?

El patrón de diseño repository es un conocido patrón en la literatura y en los contextos de arquitectura de software, el cual, básicamente, plantea una separación de las fuentes de datos del resto de la aplicación en una capa que se encarga de todo el manejo y mapeo de la información. El repositorio es, entonces, el elemento fundamental de esta capa, ya que permite encapsular la funcionalidad de acceso y manipulación la información, de forma que la gestión interna de las fuentes de datos (que pueden ser servicios web, modelos persistentes o caché) y la lógica de negocio se encapsula ante los ViewModels.

En el siguiente diagrama se explica cómo se conectan los elementos de arquitectura de Jetpack con el repositorio:

El esquema muestra una actividad, que posiblemente incluye algún fragmento, desde donde se observan los datos expuestos como LiveData en el ViewModel. El LiveData obtiene su valor consultando al repositorio, el cual tiene acceso a varias fuentes de datos como un almacenamiento local en SQLite o un servicio remoto consultado por medio de HTTP. El repositorio maneja la información por medio de modelos.

Imagen 6. Esquema de arquitectura MVVM con Jetpack

Es posible observar que el repositorio es un mediador de la información, que consulta y altera múltiples fuentes de datos según la estrategia de retrieval definida, y entrega y actualiza la información indicada por el ViewModel, el cual es la fuente de observación de datos de las vistas de la aplicación.

Actualmente, la aplicación de los coleccionistas de vinilos cuenta con una única fuente de datos, que es el servicio web del proyecto del curso de ingeniería de software para web. No obstante, incluir un repositorio para cada vista facilitará en el futuro la inclusión de otras fuentes de datos, e incluso la implementación de estrategias de caché y obtención de la información. En este tutorial, se simularán ciertas operaciones cuya explicación detallada corresponden a tutoriales posteriores.

Implementar CollectorsRepository, AlbumRepository y CommentsRepository

Podrá notar en la estructura del proyecto, que existe un directorio llamado repositories, el cual se encuentra vacío. Este será el directorio en donde ubique el código fuente de las clases que representan los repositorios de cada vista. Desde el editor de Android Studio, haga clic derecho sobre dicho directorio y seleccione la opción New > Kotlin File/Class. Cree una clase CollectorsRepository, una clase AlbumRepository, y una clase CommentsRepository. En los tres archivos creados, agregue el parámetro de clase application. Así mismo, agregue una función llamada refreshData, con el cuerpo vacío. Así, el código del que va a partir su clase debe verse como el siguiente:

package com.example.jetpack_codelab.repositories

import android.app.Application
class <RepositoryName> (val application: Application){
   fun refreshData() {}
}

Donde <RepositoryName> corresponderá exactamente con el nombre del archivo que creó (es decir, CollectorsRepository, AlbumRepository, o CommentsRepository).

Ahora, para cada repositorio, debe buscar el código del ViewModel correspondiente. Recuerde que, en ambos casos, existe un método refreshDataFromNetwork, el cual consulta directamente al NetworkServiceAdapter para obtener la información relacionada a la vista. En el caso del CollectorViewModel, este método se ve actualmente de la siguiente forma:

private fun refreshDataFromNetwork() {
      NetworkServiceAdapter.getInstance(getApplication()).getCollectors({
       _collectors.postValue(it)
       _eventNetworkError.value = false
       _isNetworkErrorShown.value = false
   },{
       Log.d("Error", it.toString())
       _eventNetworkError.value = true
   })
}

Note, nuevamente, que en el llamado a la función getCollectors del Service Adapter, se está utilizando un callback dentro de los parámetros para determinar las instrucciones que debe tomar el programa cuando la solicitud es exitosa y cuando tiene un error. Posiblemente en este momento ya ha podido suponer que este llamado al Service Adapter será trasladado al repositorio, y, junto con él, también será necesario que en el repositorio se pasen los callbacks respectivos.

Teniendo en cuenta lo anterior, debe agregar dos parámetros a la función refreshData de cada repositorio: uno para la función de callback que el ViewModel requiere para determinar qué hacer con la información que obtiene, y otro para la función de error, la cual también es necesaria para actualizar el estado del ViewModel. Estos parámetros son del tipo Unit y sus parámetros deben ser indicados como parte de la declaración del tipo. Si revisa el código del NetworkServiceAdapter, verá que las funciones getCollectors, getAlbums y getComments también cuentan con dichos parámetros.

En el caso del CollectorsRepository, debe copiar los parámetros de la función getCollectors a los parámetros de la función refreshData, de forma que el encabezado de la misma se verá de la siguiente forma:

fun refreshData(callback: (List<Collector>)->Unit, onError: (VolleyError)->Unit)

Haga lo mismo para el AlbumRepository con los parámetros del método getAlbums, y lo mismo para el CommentsRepository con los parámetros del método getComments (incluyendo el parámetro del albumId como un parámetro extra, de tipo Int, que va antes del parámetro callback).

Lo siguiente que debe hacer es modificar el contenido de la función refreshDataFromNetwork del repositorio a la misma vez que se modifica la función refreshData del repositorio. Como se explicó en el apartado anterior, el repositorio debe encapsular el acceso a las fuentes de información, las cuales en este tutorial se reducen únicamente al NetworkServiceAdapter. De esta forma, el ViewModel no debe tener ningún contacto con este último.

Copie el código de la función refreshDataFromNetwork del ViewModel y péguelo en el cuerpo de la función refreshData del repositorio. Podrá ver que el editor de Android Studio señala varios errores, pues las variables del ViewModel no existen en el repositorio. Ignore dichas advertencias y proceda a reemplazar los valores de los parámetros (Units) que le pasaba anteriormente a la función del NetworkServiceAdapter por los parámetros de su función refreshDataFromNetwork. Así, en el caso del CollectorsRepository, el código de la función refreshData se debe ver de la siguiente forma:

fun refreshData(callback: (List<Collector>)->Unit, onError: (VolleyError)->Unit) {
   //Determinar la fuente de datos que se va a utilizar. Si es necesario consultar la red, ejecutar el siguiente código
   NetworkServiceAdapter.getInstance(application).getCollectors({
           //Guardar los coleccionistas de la variable it en un almacén de datos local para uso futuro
           callback(it)
       },
       onError
   )
}

En el caso del AlbumRepository, el proceso debe ser igual, pero de forma análoga con el AlbumViewModel. Asimismo, para el CommentsRepository, el proceso debe ser igual, pero de forma análoga con el CommentsViewModel.

En este momento, el repositorio está listo para servir su función de intermediario entre el ViewModel y el NetworkServiceAdapter, ya que recibe los parámetros del primero y realiza el llamado respectivo al segundo según ciertos criterios, los cuales en este tutorial siempre serán los mismos: consultar la información directamente del servicio web. Finalmente, debe modificar el método refreshDataFromNetwork del ViewModel para que invoque al nuevo método refreshData del repositorio respectivo. Este método será el nuevo objetivo de los parámetros de tipo Unit que se instanciaban anteriormente de forma directa en el ViewModel. Luego de esta modificación, el código de dicho método en el CollectorsViewModel se debe ver de la siguiente forma:

private val collectorsRepository = CollectorsRepository(application)

...

private fun refreshDataFromNetwork() {
   collectorsRepository.refreshData({
       _collectors.postValue(it)
       _eventNetworkError.value = false
       _isNetworkErrorShown.value = false
   },{
       _eventNetworkError.value = true
   })
}

Reemplace este método de la misma forma para las otras dos clases (teniendo en cuenta el atributo albumId para la invocación del método refreshData del commentsRepository), y habrá completado la creación de sus repositorios.

Ejecute la aplicación con los cambios

Vuelva a ejecutar la aplicación y podrá ver que, a nivel funcional no existe ningún cambio. No obstante, a nivel de arquitectura está implementando un diseño más desacoplado y modular, donde se separan las responsabilidades de una forma más adecuada y donde posteriormente podrá incluir estrategias para consultar más de una fuente de datos y combinar cachés con almacenamiento local y varios servicios y protocolos de comunicación.

¡Felicidades!

Al finalizar este tutorial, pudo familiarizarse con los conceptos de Service Adapter y Repository, comprendiendo su utilidad a partir de los respectivos patrones de diseño. Así mismo, pudo comprender cómo implementarlos en un proyecto de Android en Kotlin.

Ahora podrá aplicar estos patrones de diseño a sus propios proyectos de Android, de forma que estos tengan una mayor modularidad, desacoplamiento y separación de responsabilidades.

Créditos

Versión 1.0 - Julio 28, 2021

Juan Sebastián Espitia Acero

Autor

Norma Rocio Héndez Puerto

Revisora

Mario Linares Vásquez

Revisor