Last Updated: 2021-30-05
En este tutorial se trabajará sobre una aplicación desarrollada para tutoriales anteriores. 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á:
Al desarrollar este tutorial aprenderá:
La tarea de mostrar imágenes obtenidas de internet en una aplicación de Android no es tan sencilla como en otros ambientes a causa de la naturaleza del entorno móvil. En un navegador existen herramientas que se encargan de la administración de las imágenes y los recursos multimedia sin necesidad de que el desarrollador se involucre, pero en el caso de las aplicaciones móviles el problema es distinto en cuanto a la ingeniería.
Para utilizar imágenes en una aplicación de Android es necesario descargar la imagen por medio de la red, almacenarla internamente y decodificarla de su formato comprimido. Ahora bien, el almacenamiento de las imágenes es un tema de cuidado, ya que este tipo de medios suelen ocupar una cantidad de espacio de almacenamiento.
Por este motivo, es muy importante tener en cuenta las opciones de caché en memoria y caché basado en el almacenamiento. Así mismo, es importante que todas estas operaciones sean ejecutadas en otros contextos ajenos al hilo principal, de forma que la interfaz del usuario no se vea afectada por la cantidad de datos descargados y formateados.
Este proceso no es sencillo, puesto que hay que optimizar la concurrencia de forma que se procesen múltiples imágenes a la vez, implementar estrategias de renovación del caché e interactuar de forma cautelosa con los ciclos de vida de las vistas donde se utilice dicha información.
Como se mencionó anteriormente, es necesario administrar cada imagen como un archivo dentro del sistema de archivos de Android, de forma que estos puedan estar disponibles para la aplicación y ser procesados como elementos gráficos. Dado que se busca optimizar el espacio de almacenamiento, es importante tener en cuenta la compresión de los archivos y el espacio de disco que se va a alojar.
Con lo anterior en mente, la mejor opción para manejar imágenes dentro de una aplicación en Android es hacer uso de una librería para este fin. Optar por implementar esta funcionalidad de forma manual implica un retrabajo innecesario, y hace muy propensa la aplicación a fallas. Por este motivo, en el siguiente paso se expondrán algunas de las librerías más utilizadas.
Por defecto, los proyectos creados en Android Studio no incluyen librerías ni dependencias, y es necesario agregarlas a mano para poder utilizar las herramientas que se necesitan en la aplicación.
Glide es una librería presentada por Bumptech para descargar imágenes y guardarlas en caché para ser utilizadas posteriormente por una aplicación Android. Esta librería incluye utilidades para editar las imágenes y soporta imágenes animadas en formato GIF.
Esta librería es altamente adaptable ya que se basa en un stack personalizado que utiliza HttpUrlConnection, y también se puede incorporar con otros stacks que incluyan librerías de red como Volley u OkHttp.
Glide puede incorporarse en proyectos con una versión del API de Android igual o superior al API de nivel 14, por medio de la librería disponible en el paquete com.github.bumptech.glide
.
Esta librería resalta por sus optimizaciones realizadas en pro de la fluidez de la interfaz gráfica de usuario. Estas incluyen, en primer lugar, un cacheado y un downsampling en pro de minimizar los tiempos de decodificación y el costo general de almacenamiento. Además, se caracteriza por su extensiva reutilización de recursos como arrays de bytes o mapas de bits para reducir los garbage collections. Finalmente, también manejan una integración adecuada y detallada con los ciclos de vida de los componentes de las vistas para evitar fallos en la referencia a recursos.
Picasso es otra de las librerías más populares para la administración y renderización de imágenes (tanto locales como obtenidas de internet), en una aplicación de Android. Esta librería pertenece a Square, y está disponible por medio del paquete com.squareup.picasso. Su fin también es ofrecer funcionalidades de descarga, transformación y carga de imágenes, brindando un control adicional sobre los procesos de descarga de recursos.
Cabe resaltar que el funcionamiento de su API en cuanto a la carga de imágenes en recursos de la aplicación es muy similar al de Glide. Si bien ambas opciones son bastante sólidas y tienen algunas similaridades, la librería de Glide brinda mayores beneficios para el desempeño de la aplicación y el proceso de desarrollo. Por este motivo, Glide será utilizada para este tutorial
Además de estas librerías, existen otras alternativas menos populares, las cuales se han hecho disponibles en internet en código abierto. Estas librerías incluyen, entre otras, las siguientes opciones:
El proyecto que se ha desarrollado en tutoriales anteriores no incluye soporte para poder efectuar varios de los cambios que se realizarán en este tutorial, por lo cual es necesario agregar una nueva dependencia a este proyecto.
Se hará uso de la librería Glide, por lo cual, únicamente se requiere de una dependencia en particular, alojada en un paquete de terceros llamado com.github.bumptech
. Para incluirla, es necesario que siga estos pasos:
build.gradle
del módulo :app en el editor de Android Studio.dependencies
. Agregue las siguientes líneas de código:dependencies {
implementation 'com.github.bumptech.glide:glide:4.8.0'
}
Gradle Sync
.Luego de que termine el proceso de sincronización, revise si el resultado es adecuado o hubo algún error. Es posible que existan errores relacionados con su versión de Gradle o la versión de alguna de las librerías. Si este es el caso, Android Studio le ayudará a actualizarlas a las versiones adecuadas por medio de sugerencias del Lint (la herramienta de análisis estático de código de Android Studio).
Abra el archivo album_item.xml
y modifique el contenido de forma que el LinearLayout
existente quede envuelto en otro LinearLayout
con orientación horizontal, que a su vez agregue una imagen en la parte izquierda. El código xml de este recurso, luego de los cambios, debe quedar de la siguiente forma:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="album" type="com.example.vinyls_jetpack_application.models.Album"/>
</data>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:strokeColor="@color/stroke_color"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:id="@+id/album_cover" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp">
<TextView
android:id="@+id/textView6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="3dp"
android:text="@{album.name}"
android:textStyle="bold" />
<TextView
android:id="@+id/textView5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="3dp"
android:paddingEnd="8dp"
android:paddingBottom="3dp"
android:text="@{album.description}" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</layout>
Ahora que modificó la estructura de la interfaz de la vista que contiene los álbumes, es necesario que ajuste el data binding para que la información del adapter sea mostrada de forma adecuada en la vista de álbumes.
Usualmente, el data binding lo ha manejado por medio de las variables dentro de las etiquetas <layout>
de los archivos de recursos, y por medio de referencias en los atributos de layout. No obstante, en el caso de estos ImageViews
, no utilizará dicho medio, ya que Glide requiere que se invoque una función de forma programática para asignar una imagen a un elemento ImageView
.
Para poder establecer esta relación, abra el archivo ui/adapters/AlbumsAdapter
. Allí, al final del archivo, encontrará la declaración de la clase AlbumViewHolder
. Incluya en la clase una función que ejecute el código básico para asociar una imagen con una vista de tipo ImageView
, como se muestra a continuación:
fun bind(album: Album) {
Glide.with(itemView)
.load(album.cover.toUri().buildUpon().scheme("https").build())
.apply(RequestOptions()
.placeholder(R.drawable.loading_animation)
.error(R.drawable.ic_broken_image))
.into(viewDataBinding.albumCover)
}
Note que esta operación se aplica sobre el contexto de la vista itemView, carga la imagen desde una URL construida con la ayuda de la librería androidx.core.net.toUri
, y la dibuja en el elemento con el identificador album_cover
.
Una vez haya definido esta función, haga una invocación en el método onBindViewHolder
del adaptador, en la clase AlbumsAdapter
. De esta forma, luego de incluir el llamado a su nueva función bind
, el método onBindViewHolder
se debe ver de la siguiente forma:
override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) {
holder.viewDataBinding.also {
it.album = albums[position]
}
holder.bind(albums[position])
holder.viewDataBinding.root.setOnClickListener {
val action = AlbumFragmentDirections.actionAlbumFragmentToCommentFragment(albums[position].albumId)
// Navigate using that action
holder.viewDataBinding.root.findNavController().navigate(action)
}
}
Ejecute la aplicación con los cambios y podrá ver las imágenes de portada de los álbumes como parte de los elementos de la lista.
Imagen 1. Vista de los álbumes con imágenes
Anteriormente se mencionó que el proceso de incluir imágenes en una aplicación de Android requería de varias consideraciones, las cuales eran manejadas en su mayoría por las librerías. No obstante, en este punto, no basta con la implementación que se ha realizado. Es necesario ahora declarar un comportamiento para manejar los errores al obtener las imágenes de internet, y también establecer una forma de cacheado de la información.
El comportamiento por defecto de Glide involucra varias consultas a caché antes de iniciar una petición de red por una imagen. En primer lugar, Glide internamente averigua si la imagen está siendo utilizada en otra vista; luego, consulta el caché de memoria, a ver si la imagen se ha cargado recientemente; después, consulta por el recurso decodificado y transformado, que se ha escrito en el caché del disco, y finalmente por el recurso codificado.
Para cada etapa existe un caché que almacena información sobre el tamaño, las transformaciones de Glide aplicadas, el tipo de dato, y las opciones seleccionadas.
La primera configuración de caché a mencionar es la estrategia de cacheado en disco. Esta puede ser configurada con el método .diskCacheStrategy()
, y acepta valores para almacenar en el caché de disco la información de datos crudos y/o recursos decodificados, o ninguno.
La segunda configuración fuerza a Glide a utilizar imágenes únicamente si están en el caché. Esto se logra con el método .onlyRetrieveFromCache()
.
La tercera configuración fuerza a Glide a saltar las consultas de caché de disco o de memoria. Para el primer caso, se logra implementando la estrategia de caché de disco DiskCacheStrategy.NONE
. Para el segundo caso, se utiliza el método .skipMemoryCache()
. Esta configuración no es recomendada, ya que haría que se pierdan varios de los beneficios de utilizar cachés para acceder a los datos almacenados con rapidez.
En el caso de este tutorial, conviene almacenar todas las imágenes y datos en caché. Incluya esta configuración en el ViewHolder
, de forma que el código del método bind
se vea de la siguiente forma:
fun bind(album: Album) {
Glide.with(itemView)
.load(album.cover.toUri().buildUpon().scheme("https").build())
.apply(RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.ALL))
.into(viewDataBinding.albumCover)
}
Por último, es importante que, en caso de presentarse errores al obtener y mostrar las imágenes, el usuario siga teniendo una experiencia adecuada. Por este motivo, se debe configurar una imagen por defecto para mostrar cuando todavía no se ha cargado el recurso solicitado, y otra imagen por defecto para mostrar en caso de que no sea posible cargar dicho recurso.
Glide maneja esta configuración por medio de la clase RequestOptions
. Para agregar este comportamiento, solo debe agregar una invocación al método placeholder
, y una invocación al método error en el objeto RequestOptions
ya existente. El código de su método bind
, finalmente, debe verse de la siguiente forma:
fun bind(album: Album) {
Glide.with(itemView)
.load(album.cover.toUri().buildUpon().scheme("https").build())
.apply(RequestOptions()
.placeholder(R.drawable.loading_animation)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.error(R.drawable.ic_broken_image))
.into(viewDataBinding.albumCover)
}
Si vuelve a ejecutar su aplicación, posiblemente no note ninguna diferencia a simple vista, pero el código que se implementó con Glide permite que el manejo de sus imágenes sea adecuado y óptimo.
¡Felicidades!
Al finalizar este tutorial pudo familiarizarse con el proceso para incluir imágenes dentro de su aplicación Android. Así mismo, pudo conocer y realizar su primera implementación gracias a la librería Glide.
Ahora podrá explorar nuevos recursos y seleccionar una librería adecuada para incluir imágenes en sus aplicaciones Android, además de asignar estrategias de caché para las mismas.
Juan Sebastián Espitia Acero | Autor |
Norma Rocio Héndez Puerto | Revisora |
Mario Linares Vásquez | Revisor |