Last Updated: 2021-11-02

¿Por qué usar Android Jetpack?

Jetpack es una serie de bibliotecas para Android que fueron desarrolladas para facilitar la creación de aplicaciones potentes y óptimas en términos de arquitectura, mantenibilidad, separación de responsabilidades y tiempo de desarrollo, con menos código. Estas librerías están construidas en torno a prácticas de diseño modernas, que tienen en cuenta el ciclo de vida del desarrollo de software y, a su vez, la importancia de las pruebas en el software. Además, con Jetpack se puede tener compatibilidad hacia versiones anteriores de Android de forma muy sencilla.

Si desea ver la presentación oficial de Android Jetpack para conocer una idea general de lo que este brinda, ingrese al siguiente enlace: https://www.youtube.com/watch?v=LmkKFCfmnhQ&ab_channel=AndroidDevelopers.

¿Qué construirá?

Al final de este tutorial usted tendrá:

¿Qué aprenderá?

Al desarrollar este tutorial aprenderá:

¿Qué necesita?

Generalidades

Las librerías de Jetpack agrupan las support libraries, que contienen componentes fundamentales con soporte a versiones anteriores, con nuevos componentes que permiten crear aplicaciones sólidas en Android. Estas no son parte de la plataforma de Android pero facilitan la interacción con las librerías que sí lo son y permiten hacerlo de forma desagregada, ya que están definidas en un segmento aparte denominado Android X. Las múltiples librerías de Jetpack se pueden categorizar en 4 grupos, como se muestra en la imagen a continuación:

Infografía que cataloga en 4 categorías a las librerías de Jetpack: UI, Architecture, Behavior, Foundation. Explicación en el párrafo siguiente\n

Imagen 1. Categorización de las librerías de Jetpack

Componentes de arquitectura de Android para MVVM

En el contexto de este tutorial, un foco central de las librerías de Jetpack está en los elementos que permiten manejar la interacción de los componentes de Android. Con Jetpack se pueden diseñar e implementar aplicaciones con el patrón de arquitectura MVVM sin necesidad de implementar mucho código boilerplate (es decir, del código fuente que suele no variar según la implementación), ya que estas librerías estandarizan el comportamiento de los elementos Modelo, Vista y ViewModel.

En primer lugar, se tienen las representaciones de modelos de datos y vistas de usuario del lenguaje de programación. Los modelos de datos son creados a partir de elementos primitivos del lenguaje como lo son los data classes, ya que permiten definir las entidades y sus propiedades de forma muy sencilla. Por su parte, las vistas de usuario son manejadas con los componentes como Fragmentos y Actividades, los cuales se han tratado en tutoriales anteriores, y son parte fundamental de los componentes de una aplicación de Android.

Finalmente, el elemento más importante en este punto es el ViewModel, el cual permite conectar las fuentes de datos con los componentes que las consultan en la interfaz gráfica. La clase ViewModel de Android Jetpack permite administrar los datos de la interfaz teniendo muy presente los ciclos de vida de los componentes, de forma que permiten preservar el estado de los datos luego de cambios en la configuración de la aplicación. Por lo general, un ViewModel se solicita la primera vez que el sistema llama al método onCreate() de una vista, y se destruye en el momento en que la vista es finalizada y destruida, como se muestra en el siguiente diagrama, tomado de la documentación de Android Developers:

Esquema que muestra las etapas de una actividad y los métodos que se llaman en cada una: Activity created(métodos onCreate, onStart, onResume). Activity rotated (métodos onPause, onStop, onDestroy, onCreate, onStart, onResume). Activity finish() (onPause, onStop, onDestroyed). \nEl ViewModel tiene un scope existente desde el primer método llamado hasta el último, y llama la función onCleared una vez la actividad se encuentra en estado finalizada\n

Imagen 2. Ciclo de vida del ViewModel de una vista

Observadores de datos

Entre los componentes de arquitectura, también se pueden incluir los componentes y librerías que permiten que la aplicación se actualice a partir de información que cambia constantemente, ya que facilitan la implementación del patrón de diseño Observer, y el diseño de interfaces gráficas para mostrar colecciones de datos.

En primer lugar, vale la pena mencionar las clases LiveData y MutableLiveData de Android, las cuales son contenedores de datos (estáticos o variables) optimizados para los diferentes ciclos de vida de los componentes de una aplicación, de forma que solo los componentes con un estado de ciclo de vida activo pueden actualizar sus observadores de datos.

Estos LiveData están estrechamente vinculados a los objetos de la clase Observer de Android, la cual existe con el único propósito de definir el comportamiento de los LiveData cuando existe una actualización en los datos que esta contiene. Los observadores suelen ser instanciados en un controlador de la interfaz gráfica de usuario, el cual actuará como el dueño del ciclo de vida. El funcionamiento de estos observadores se explica a grandes rasgos en el siguiente diagrama:

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 3. Esquema de arquitectura MVVM con Jetpack

Por otra parte, es de resaltar la librería de Data Binding, la cual permite vincular los componentes de la interfaz gráfica a las fuentes de datos haciendo uso de un formato declarativo que reemplaza la necesidad de programar imperativamente la actualización de datos en la vista. Para hacer uso de Data Binding, es necesario declarar a nivel de proyecto que se utilizará esta característica, de forma que al momento de construir la aplicación con Gradle se generen las clases de vinculación correspondientes, que permitan a la aplicación vincular los componentes de UI con sus componentes funcionales vinculados, declarados en los archivos de recursos del directorio res. El detalle de este proceso será explicado en un paso siguiente de este tutorial.

Finalmente, cabe aclarar que para esta vinculación de los datos con la interfaz también se cuenta con un componente fundamental, denominado RecyclerView, y con sus asociadas librerías de paginación para cargar los datos de forma gradual en componentes gráficos que permiten mostrar colecciones de ítems minimizando el consumo de memoria y de recursos de procesamiento al momento de renderizar grandes cantidades de datos en la interfaz.

Otras funcionalidades

Aunque las librerías de Jetpack tienen múltiples propósitos específicos, vale la pena resaltar algunas de ellas dada la relevancia que tienen en la construcción de aplicaciones móviles que sigan patrones de diseño recomendados, y dado el valor que aportan a la aplicación en términos de desempeño y funcionalidad. La primera de estas librerías se mencionó en el apartado inmediatamente anterior, y es la librería Paging, la cual facilita que los datos que se mostrarán en la interfaz gráfica de usuario por medio de un RecyclerView sean separados en secciones o páginas, de forma que la memoria utilizada por el dispositivo móvil también es optimizada, y el proceso de renderizado en la interfaz también se hace de forma gradual.

Además de esta, existe una librería fundamental para los componentes de persistencia, durante la construcción de aplicaciones de Android con un patrón MVVM, llamada Room. Esta librería brinda una capa de abstracción a la aplicación para hacer uso de una base de datos SQLite que existe localmente en el dispositivo móvil. De esta forma, se puede hacer almacenamiento de cualquier información de forma local, lo que permite crear estrategias de manejo de Caché y acceso a información sin conexión a internet. Además, Room encapsula las funcionalidades de forma que no es necesario definir esquemas y manejar consultas sobre la información con el lenguaje SQL desde la aplicación móvil.

Información de interés del API REST

Para este tutorial se utilizará el API REST del proyecto de los vinilos, el cual también fue utilizado para el tutorial de invocación de APIs REST en Android. En este caso, el enfoque de este acercamiento no será orientado hacia el uso de las librerías y los métodos HTTP, sino hacia la obtención de información de forma dinámica y su respectiva carga a la interfaz gráfica de usuario. Para usar las funcionalidades completas de Jetpack y del flujo de navegación, se decide utilizar la información de tres clases que tienen relaciones entre ellas, según el diagrama de clases de la documentación (detallado a fondo en el siguiente enlace: https://misw-4104-web.github.io/GuiasProyecto/generalidades.html#documentaci%C3%B3n-del-api), el cual se muestra a continuación:

Sección del diagrama de clases que contiene las clases relacionadas con los coleccionistas, los álbumes y los comentarios.

Imagen 4. Sección de interés del diagrama de clases del problema

Aunque en la imagen se vean 6 clases señaladas, estas representan únicamente tres recursos del API REST: Collectors, Comments y Albums. De estos recursos, únicamente se hará uso de los métodos GET para obtener la información correspondiente a los coleccionistas, los álbumes, los comentarios, y el método POST para crear comentarios. En detalle, los recursos a consumir son los siguientes:

Planeación de las vistas

Como se mencionó anteriormente, los endpoints elegidos ayudarán a explorar en mayor medida las funcionalidades de Jetpack y los flujos de navegación, ya que permiten ver los elementos que pertenecen a otros en un camino de usabilidad que solo tiene un sentido lógico en términos de pertenencia: coleccionistas, luego álbumes, luego comentarios. Esto se puede traducir a tres pantallas, donde se pueda ver un listado con estos elementos para elegir, y que al elegir, se pase a la siguiente lista. Así, la primera pantalla corresponde a la lista de coleccionistas, donde se elige el "perfil" del coleccionista que revisará y creará comentarios. Luego, la segunda pantalla corresponde a la lista de álbumes de la aplicación, donde se puede ver su información y al seleccionar uno, se pasa a la tercera pantalla, donde se podrán ver y crear comentarios al respecto del álbum.

Un diseño de wireframe para la aplicación puede ser el que se expone en las siguientes imágenes:

Esquema inicial del contenido de las tres vistas que se van a implementar. De izquierda a derecha, la vista de coleccionistas, que muestra un listado con la información de los coleccionistas; seguida de la vista de álbumes, que muestra su nombre y descripción, y finalmente la vista de comentarios del álbum

Imagen 5. Wireframe de las vistas de la aplicación

Obtención y exploración del código fuente base

El código inicial del proyecto se encuentra alojado en un repositorio de GitHub, al cual puede acceder desde el siguiente enlace: https://github.com/TheSoftwareDesignLab/MISW4104-Ejemplos/tree/main/starters. Ingrese al enlace anterior para ver el código del proyecto en el directorio starters. 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:

Sección del sitio web del repositorio donde se muestran las diferentes opciones para descargar el código del proyecto> Clone, Open with Github Desktop, Download ZIP

Imagen 6. 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, 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 imagen 7 a continuación.

Abra Android Studio para importar el proyecto. Si no tiene un proyecto previamente abierto, verá un menú como el de la imagen 7; en este caso, presione la opción "Import project". En caso de tener un proyecto abierto, debe buscar la opción "New Project... > Import Project" del menú de la parte superior del IDE en la pestaña "File", la cual se muestra en la imagen 8 a continuación:

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 7. Menú de inicio de Android Studio

Menú de paneles de la parte superior del IDE de Android Studio con la pestaña File abierta y la opción \

Imagen 8. Menú superior de Android Studio

En ambos casos, verá una ventana para seleccionar la ubicación de su proyecto. Seleccione la ubicación donde lo descargó.

Explore el contenido de su proyecto en este punto. Podrá ver que contiene una estructura básica con varios directorios vacíos y métodos pendientes por desarrollar. Los elementos que se le entregan como base comprenden los archivos a nivel de proyecto, como lo son:

  1. El archivo AndroidManifest.xml con la actividad principal y el permiso de internet declarados.
  2. Los archivos build.gradle a nivel de proyecto y a nivel de módulo, detallando la implementación específica de las dependencias requeridas para utilizar Jetpack y la librería Volley. Note que en el archivo a nivel de módulo (app/build.gradle), se encuentran varias líneas de código en la sección dependencies que incluyen implementaciones de librerías que comienzan por androidx.*. Estas librerías hacen parte de Jetpack y varias son incluidas por defecto al momento de crear una aplicación de Android X. Además, podrá ver la implementación de la librería Volley, tal como en el tutorial de frameworks de invocación de APIs REST.
  3. El archivo navigation.xml con el gráfico de navegación entre los diferentes fragmentos de la aplicación.
  4. Los archivos de recursos de interfaz gráfica en el directorio res/layout.

Lo primero que hará para construir la aplicación será crear los modelos de datos en un nuevo directorio llamado models del código fuente. Este paso es muy sencillo, ya que solo necesita declarar un data class con los atributos a modelar de cada una de las tres entidades que se manejan en este contexto.

Dichos atributos los puede consultar en el modelo de clases de la imagen 4..

Luego de validar los datos que se necesitan, proceda a crear tres clases: una con el nombre Album, una con el nombre Collector y otra con el nombre Comment, haciendo clic derecho sobre el directorio models en Android Studio y seleccionando la opción New > Kotlin File/Class. En el caso de la clase Album, este debe ser el contenido:

data class Album (
   val albumId:Int,
   val name:String,
   val cover:String,
   val releaseDate:String,
   val description:String,
   val genre:String,
   val recordLabel:String
)

Para la clase Collector, este debe ser el contenido:

data class Collector (
   val collectorId: Int,
   val name:String,
   val telephone:String,
   val email:String
)

Y en el caso de la clase Comment, este debe ser el contenido:

data class Comment (
   val description:String,
   val rating:String,
   val albumId:Int
)

Luego de definir los modelos de datos, es necesario conectar las fuentes de datos del componente encargado de la red con la interfaz gráfica. Para esto, desde Android Studio cree un directorio viewmodels en el código fuente de la aplicación, haga clic derecho en él y con la opción New > Kotlin File/Class agregue un archivo para cada una de las entidades para las cuales creó un modelo, es decir, un ViewModel para los álbumes, otro para los coleccionistas y otro para los comentarios.

Para los tres casos, usted utilizará la misma estructura para crear la clase que maneja el ViewModel. Esta consiste en una clase que:

  1. tiene como parámetro un objeto de tipo Application,
  2. extiende de la clase AndroidViewModel,
  3. tiene un método refreshDataFromNetwork(), el cual se encarga de consultar activamente la información de los modelos con el manejador de peticiones de red y actualizar los LiveData respectivos.
  4. contiene una clase interna llamada Factory, la cual extiende de ViewModelProvider.Factory y se encarga de crear la instancia del ViewModel.
  5. contiene varias parejas de variables, donde una de ellas es del tipo MutableLiveData y se utiliza para representar la fuente dinámica de datos, y la otra de tipo LiveData se utiliza para representar el valor almacenado de los datos en un momento.

Una de las parejas de datos mencionadas es de tipo List y contiene la colección de datos del modelo representado en cada ViewModel (Album, Collector o Comment), y también se utilizan dos parejas más para representar si existe un error de red en el manejador de las peticiones de red, y para representar si dicho error está siendo mostrado al usuario en la interfaz.

En cada uno de los archivos, copie el código fuente que se muestra a continuación y realice la refactorización necesaria, es decir, cambiar el modelo por el que corresponda a cada archivo, el nombre de las variables, y la función del NetworkServiceAdapter que es llamada para obtener los datos. Particularmente, en el caso de los coleccionistas, el método es getCollectors, y en el caso de los comentarios, el método es getComments:

class AlbumViewModel(application: Application) :  AndroidViewModel(application) {

    private val _albums = MutableLiveData<List<Album>>()

    val albums: LiveData<List<Album>>
        get() = _albums

    private var _eventNetworkError = MutableLiveData<Boolean>(false)

    val eventNetworkError: LiveData<Boolean>
        get() = _eventNetworkError

    private var _isNetworkErrorShown = MutableLiveData<Boolean>(false)

    val isNetworkErrorShown: LiveData<Boolean>
        get() = _isNetworkErrorShown
    
    init {
        refreshDataFromNetwork()
    }

    private fun refreshDataFromNetwork() {
        NetworkServiceAdapter.getInstance(getApplication()).getAlbums({
            _albums.postValue(it)
            _eventNetworkError.value = false
            _isNetworkErrorShown.value = false
        },{
            _eventNetworkError.value = true
        })
    }

    fun onNetworkErrorShown() {
        _isNetworkErrorShown.value = true
    }
    
    class Factory(val app: Application) : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>): T {
            if (modelClass.isAssignableFrom(AlbumViewModel::class.java)) {
                @Suppress("UNCHECKED_CAST")
                return AlbumViewModel(app) as T
            }
            throw IllegalArgumentException("Unable to construct viewmodel")
        }
    }
}

Para el caso del CommentViewModel es necesario que obtenga el valor del albumId seleccionado, de forma que este archivo se distinguirá del resto, de la siguiente forma:

class CommentViewModel
(application: Application, albumId: Int) :  AndroidViewModel(application) {
val id:Int = albumId

...
class Factory(val app: Application, val albumId: Int) : ViewModelProvider.Factory {
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(CommentsViewModel::class.java)) {
           @Suppress("UNCHECKED_CAST")
           return CommentViewModel(app, albumId) as T
       }
       throw IllegalArgumentException("Unable to construct viewmodel")
   }
}

}

Este parámetro id lo debe utilizar al momento de llamar al método getComments.

Para permitir el Data Binding en un proyecto de Android es necesario especificar en el archivo build.gradle que se hará uso de esta característica. Abra dicho archivo y note que en la sección android hay un apartado llamado databinding con la propiedad enabled en el valor true, como se muestra en el siguiente fragmento:

android {
...
    dataBinding {
       enabled true
    }
}

Además de asegurarse de contar con esto, debe modificar el contenido de los archivos de layout que representan un ítem de los distintos RecyclerView que se utilizarán en la aplicación. En los archivos album_item.xml, collector_item.xml y comment_item.xml podrá ver que el elemento raíz es de tipo com.google.android.material.card.MaterialCardView. Con la ayuda de Android Studio usted hará que estos archivos sean aptos para realizar Data Binding, haciendo clic derecho en el elemento raíz, seleccionando la opción "Show Context Actions", y seleccionando la opción "Convert to data binding layout", como se muestra en la siguiente imagen:

menú desplegable en el Constraint Layout, que muestra la opción de convertir a un layout con data binding\n

Imagen 9. Sugerencia de Android Studio para transformar el layout

Podrá ver que el elemento raíz ahora es una etiqueta de tipo <layout>, y que encima del contenedor original hay una etiqueta <data>. En esta sección usted definirá las expresiones y las propiedades que utilizará en su layout, y puede asociarlas con los ViewModels que creó anteriormente. Proceda a crear las variables que se requieren en cada ítem.

Para el caso del archivo collector_item.xml, las variables que van dentro de la etiqueta <data> son las siguientes:

<data>
                <variable name="collector" type="com.example.vinyls_jetpack_application.models.Collector"/>
</data>

Y las modificaciones que se requieren en la interfaz para mostrar estos datos se detallan a continuación:

<TextView
                android:id="@+id/textView2"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{collector.name}" />
<TextView
                android:id="@+id/textView3"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{collector.email}" />
<TextView
                android:id="@+id/textView4"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{collector.telephone}" />

Para el caso del archivo album_item.xml, las variables que van dentro de la etiqueta <data> son las siguientes:

<data>
        <variable name="album" type="com.example.vinyls_jetpack_application.models.Album"/>
</data>

Y las modificaciones que se requieren en la interfaz para mostrar estos datos se detallan a continuación:

<TextView
                android:id="@+id/textView6"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{album.name}" />
<TextView
                android:id="@+id/textView5"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{album.description}" />

Para el caso del archivo comment_item.xml, las variables que van dentro de la etiqueta <data> son las siguientes:

<data>
        <variable name="comment" type="com.example.vinyls_jetpack_application.models.Comment"/>
</data>

Y las modificaciones que se requieren en la interfaz para mostrar estos datos se detallan a continuación:

<TextView
                android:id="@+id/textView6"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{comment.rating}" />
<TextView
                android:id="@+id/textView5"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@{comment.description}" />

Note que en los tres casos, se modifican las propiedades de las etiquetas XML directamente desde el archivo de recursos para corresponder a los valores del Data Binding, y que estos valores se acceden con el formato @{prop}, sin necesidad de actualizar la interfaz gráfica con estos valores de forma programática.

Luego de hacer estos cambios en los archivos de recursos, será necesario establecer el DataBinding en los archivos correspondientes a las vistas que manejarán el ciclo de vida. Estas modificaciones se explican en el último paso de este tutorial.

Si desea comprender más acerca del DataBinding en Android, puede consultar y desarrollar el tutorial de Android Developers, disponible en el siguiente enlace: https://developer.android.com/codelabs/android-databinding#2.

Definir los adaptadores de los Recycler Views

Para completar el proceso del Data Binding, el paso a seguir es definir los elementos del tipo RecyclerView.Adapter para cada una de las listas de elementos que se van a incluir en la aplicación. Para esto, primero cree un directorio llamado adapters en el directorio ui del código fuente, el cual va a contener los adaptadores tanto para los álbumes, como los coleccionistas y los comentarios. Dentro de este directorio cree tres archivos: AlbumsAdapter, CollectorsAdapter y CommentsAdapter. El contenido de los tres archivos se basa en una misma estructura, por lo cual el proceso a seguir se explica con el archivo CollectorsAdapter.kt, y luego se debe refactorizar para el caso de los álbumes y los comentarios. A continuación se expone la estructura de un Adapter.

En primer lugar, la clase debe extender de RecyclerView.Adapter, especificando el tipo de ViewHolder que esta contiene, de forma que el encabezado donde se declara la clase se debe ver de la siguiente forma:

class CollectorsAdapter : RecyclerView.Adapter<CollectorsAdapter.CollectorViewHolder>(){
...
}

Como se puede deducir, ya que el tipo de ViewHolder es CollectorsAdapter.CollectorViewHolder, esta clase va a tener una clase interna para definir el ViewHolder, por lo cual este será el segundo paso. Defina la clase interna, del tipo RecyclerView.ViewHolder e incluya la vista raíz como argumento de la superclase, y el Data Binding de su vista como argumento de la clase. El contenido de esta clase será únicamente un companion object con una referencia al recurso de interfaz que representa un ítem. El código de esta clase interna se ve de la siguiente forma:

class CollectorViewHolder(val viewDataBinding: CollectorItemBinding) :
            RecyclerView.ViewHolder(viewDataBinding.root) {
        companion object {
            @LayoutRes
            val LAYOUT = R.layout.collector_item
        }
    }

El siguiente paso es definir una variable que represente la lista con los datos de los elementos que se van a mostrar en la interfaz. Incluya esta variable de clase de la siguiente forma:

var collectors :List<Collector> = emptyList()
        set(value) {
            field = value
            notifyDataSetChanged()
        }

Posteriormente, debe sobreescribir varios métodos de la superclase Adapter para definir el comportamiento de su RecyclerView. Estos métodos incluyen los métodos del ciclo de vida onCreateViewHolder y onBindViewHolder, y el método getItemCount. En el primero, se retorna la instancia del ViewHolder, creada a partir del constructor y el DataBindingUtil, que ayuda a pasar el parámetro de la clase como se indicó anteriormente. En el segundo, se definen las propiedades del ViewHolder, incluyendo la lista de datos y los listeners de los elementos de la interfaz gráfica. En el tercero, únicamente se debe realizar un cálculo que permita retornar el número de elementos en la colección de datos. Incluya estos tres métodos de la siguiente forma:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollectorViewHolder {
        val withDataBinding: CollectorItemBinding = DataBindingUtil.inflate(
                LayoutInflater.from(parent.context),
                CollectorViewHolder.LAYOUT,
                parent,
                false)
        return CollectorViewHolder(withDataBinding)
    }

    override fun onBindViewHolder(holder: CollectorViewHolder, position: Int) {
        holder.viewDataBinding.also {
            it.collector = collectors[position]
        }
        holder.viewDataBinding.root.setOnClickListener {
            val action = CollectorFragmentDirections.actionCollectorFragmentToAlbumFragment()
            // Navigate using that action
            holder.viewDataBinding.root.findNavController().navigate(action)
        }
    }

    override fun getItemCount(): Int {
        return collectors.size
    }

Note que, en el Click Listener del elemento raíz de la interfaz que representa un ítem, se realiza una operación de navegación a otro fragmento haciendo uso de las conexiones que existen entre los fragmentos en el gráfico de navegación y las direcciones generadas a partir de los Safe Args. Si desea consultar más acerca del proceso de navegación en Android Jetpack, ingrese al siguiente enlace y lea la documentación, provista por Android Developers: https://developer.android.com/guide/navigation/navigation-pass-data?hl=es-419.

Ahora repita el proceso anterior para los archivos AlbumsAdapter.kt y CommentsAdapter.kt. La refactorización puede hacerla partiendo de un reemplazo sencillo con Ctrl+R (en el editor de Android Studio) de las palabras exactas "Collector" por "Album" y por "Comment" según el caso, teniendo en cuenta las mayúsculas, y reemplazar las mismas palabras con las iniciales en minúsculas. Recuerde que esto funciona dado que el nombramiento de View Holders, Adapters, archivos de recursos de layout, propiedades de la clase (listas de datos), y las clases generadas por el Data Binding, sigue una estructura estándar en este tutorial.

Luego de esto, debe actualizar ciertos imports para contar con las referencias adecuadas a las clases utilizadas. Además, debe modificar el método onBindViewHolder, ya que la transición a efectuar cuando se seleccione un elemento de la lista no siempre es la misma. En el caso de los álbumes, esta acción debe corresponder a actionAlbumFragment2ToCommentsFragment2, mientras que en el caso de los comentarios, no debe existir la línea de código que configura el Click Listener del elemento.

En el caso de la transición de los álbumes a los comentarios, es necesario que pase un argumento que indique el identificador del álbum seleccionado. De esta forma, la variable action del clickListener de cada elemento tendrá el siguiente valor:

val action = AlbumFragmentDirections.actionAlbumFragment2ToCommentsFragment2(albums[position].albumId)

Vincular las vistas con las fuentes de datos

Finalmente, luego de tener los adaptadores y los ViewModels requeridos para las tres vistas relevantes, usted debe configurar las vistas (en este caso, los tres fragmentos). Pero antes, note que en el archivo MainActivity.kt existe un código en el método onCreate que utiliza el View Binding para configurar el NavHostFragment, el cual permite vincular el gráfico de navegación entre fragmentos y la actividad principal, y establecer el fragmento inicial; como se muestra a continuación:

Código de la clase MainActivity. Disponible en el esqueleto del proyecto. Se resalta la asignación de un atributo llamado NavController, obtenido a través del NavHostFragment

Imagen 10. Clase MainActivity

Ahora, abra el archivo CollectorFragment.kt. Podrá ver que hay varios métodos sin implementar y con la anotación TODO para indicar que están pendientes por desarrollar. Lo primero que debe hacer es definir 5 propiedades de la clase del fragmento, que incluyen dos variables para el View Binding, una para el RecyclerView, una para el ViewModel, y otra para el adaptador del RecyclerView. La definición de estas propiedades, al inicio de la clase se debe ver de la siguiente forma:

private var _binding: CollectorFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var recyclerView: RecyclerView
private lateinit var viewModel: CollectorViewModel
private var viewModelAdapter: CollectorsAdapter? = null

Luego de esto, en el método onCreateView, debe inflar el valor de la variable _binding, inicializar el Adapter del RecyclerView, y retornar la vista raíz del view binding. Esto se traduce al siguiente código:

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = CollectorFragmentBinding.inflate(inflater, container, false)
        val view = binding.root
        viewModelAdapter = CollectorsAdapter()
        return view
    }

Una vez termina de ejecutar este método del ciclo de vida, se ejecuta el método onViewCreated, donde debe asignar el valor a la variable recyclerView, y configurar su manejador de layout y su adapter para que corresponda con el de la variable viewModelAdapter. Esto se traduce al siguiente código:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        recyclerView = binding.fragmentsRv
        recyclerView.layoutManager = LinearLayoutManager(context)
        recyclerView.adapter = viewModelAdapter
}

Después de esto, debe configurar los observadores de los ViewModels para que la interfaz gráfica pueda reaccionar a los cambios en los datos. Para esto, en el método onActivityCreated, debe inicializar el valor de la variable viewModel, haciendo uso del ViewModelProvider y la clase Factory que creó en el ViewModel correspondiente, y luego asignar observadores al atributo collectors y al atributo eventNetworkError del ViewModel, de forma que, en el primer observador, actualice la información del RecyclerView, y en el segundo observador muestre un mensaje de error de red con el método onNetworkError. Esto se traduce al siguiente código:

override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val activity = requireNotNull(this.activity) {
            "You can only access the viewModel after onActivityCreated()"
        }
        viewModel = ViewModelProvider(this, CollectorViewModel.Factory(activity.application)).get(CollectorViewModel::class.java)
        viewModel.collectors.observe(viewLifecycleOwner, Observer<List<Collector>> {
            it.apply {
                viewModelAdapter!!.collectors = this
            }
        })
        viewModel.eventNetworkError.observe(viewLifecycleOwner, Observer<Boolean> { isNetworkError ->
            if (isNetworkError) onNetworkError()
        })
    }

Finalmente, debe implementar los métodos onDestroyView y el método onNetworkError. En el primer método lo único que debe hacer es borrar el valor de la variable _binding, mientras que en el segundo método debe consultar el valor de la variable isNetworkErrorShown del ViewModel, para mostrar un mensaje en la interfaz por medio de un elemento SnackBar, y actualizar el ViewModel para que sepa que se mostró el error de forma efectiva. Esto se traduce al siguiente código:

override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    private fun onNetworkError() {
        if(!viewModel.isNetworkErrorShown.value!!) {
            Toast.makeText(activity, "Network Error", Toast.LENGTH_LONG).show()
            viewModel.onNetworkErrorShown()
        }
    }

Luego de terminar con el CollectorFragment, nuevamente podrá refactorizar el código para utilizarlo en los demás fragmentos, ya que los tres contienen únicamente un RecyclerView con información obtenida de una fuente particular.

Finalmente, asigne en la clase CommentFragment el valor del albumId en el ViewModel por medio de los argumentos de navegación. Este código se representa de la siguiente forma:

override fun onActivityCreated(savedInstanceState: Bundle?) {
   super.onActivityCreated(savedInstanceState)
   val activity = requireNotNull(this.activity) {
       "You can only access the viewModel after onActivityCreated()"
   }
   activity.actionBar?.title = getString(R.string.title_comments)
   val args: CommentFragmentArgs by navArgs()
   Log.d("Args", args.albumId.toString())
   viewModel = ViewModelProvider(this, CommentViewModel.Factory(activity.application, args.albumId)).get(CommentViewModel::class.java)
   viewModel.comments.observe(viewLifecycleOwner, Observer<List<Comment>> {
       it.apply {
           viewModelAdapter!!.comments = this
           if(this.isEmpty()){
               binding.txtNoComments.visibility = View.VISIBLE
           }else{
               binding.txtNoComments.visibility = View.GONE
           }
       }
   })
   viewModel.eventNetworkError.observe(viewLifecycleOwner, Observer<Boolean> { isNetworkError ->
       if (isNetworkError) onNetworkError()
   })
}

¡Felicidades!

Al finalizar este tutorial, pudo familiarizarse con las librerías y la estructuración de un proyecto con la arquitectura de Jetpack para desarrollar una aplicación que muestra datos obtenidos de forma dinámica.

Ahora podrá incorporar dichas librerías y principios de diseño a sus aplicaciones para desarrollar aplicaciones dinámicas robustas y mantenibles. Además, podrá explorar otras funcionalidades ofrecidas por Jetpack para expandir las funcionalidades de sus aplicaciones de Android.

Créditos

Versión 1.3 - Noviembre 2, 2021

Juan Sebastián Espitia Acero

Autor

Norma Rocio Héndez Puerto

Revisora

Mario Linares Vásquez

Revisor