Cómo cargar y mostrar imágenes de Internet

Introducción

En el codelab anterior, aprendiste cómo obtener datos de un servicio web y analizar la respuesta en un objeto Kotlin. En este codelab, aprenderás a cargar y mostrar fotos desde una URL web. También puedes revisar cómo compilar un objeto RecyclerView y usarlo para mostrar una cuadrícula de imágenes en la página de descripción general.

Requisitos previos

  • Cómo crear y usar fragmentos
  • Cómo recuperar JSON de un servicio web de REST y analizar esos datos en objetos de Kotlin con las bibliotecas Retrofit y Moshi
  • Cómo construir un diseño de cuadrícula con RecyclerView
  • Cómo funcionan Adapter, ViewHolder y DiffUtil

Qué aprenderás

  • Cómo usar la biblioteca Coil para cargar y mostrar una imagen desde una URL web
  • Cómo usar RecyclerView y un adaptador de cuadrícula para mostrar una cuadrícula de imágenes
  • Cómo manejar los posibles errores mientras se descargan y se muestran las imágenes

Qué compilarás

  • Modificarás la app de MarsPhotos para obtener la URL de la imagen de los datos de Marte y usarás Coil para cargar y mostrar esa imagen.
  • Agregarás una animación de carga y un ícono de error a la app.
  • Usarás RecyclerView para mostrar una cuadrícula de imágenes de Marte.
  • Agregarás administración de estado y errores a RecyclerView.

Requisitos

  • Una computadora con un navegador web moderno, como la versión más reciente de Chrome
  • Tener acceso a Internet en la computadora

En este codelab, continuarás trabajando con la app del codelab anterior, MarsPhotos. La app de MarsPhotos se conecta a un servicio web para recuperar y mostrar la cantidad de objetos de Kotlin recuperados con Retrofit. Estos objetos de Kotlin contienen las URL de las fotos reales de la superficie de Marte capturadas por los rovers de la NASA.

La versión de la app que compilarás en este codelab completará la página de descripción general, la cual muestra fotos de Marte en una cuadrícula de imágenes. Las imágenes son parte de los datos que tu app recuperó del servicio web de Marte. Tu app usará la biblioteca de Coil para cargar y mostrar las imágenes, y un RecyclerView a fin de crear el diseño de cuadrícula para las imágenes. Además, la app manejará correctamente los errores de red.

1b33675b009bee15.png

Mostrar una foto de una URL web puede parecer sencillo, pero se necesita un poco de ingeniería para que funcione bien. La imagen se debe descargar, almacenar de forma interna y decodificar de su formato comprimido a una imagen que Android pueda usar. La imagen debe almacenarse en una memoria caché, en una caché basada en almacenamiento o ambas. Todo esto tiene que ocurrir en subprocesos en segundo plano de baja prioridad para que la IU siga siendo receptiva. Además, para obtener el mejor rendimiento de red y CPU, te recomendamos recuperar y decodificar más de una imagen a la vez.

Afortunadamente, puedes usar una biblioteca creada por la comunidad llamada Coil para descargar, almacenar en búfer, decodificar y almacenar en caché tus imágenes. Sin usar Coil, tendrías mucho más trabajo.

Básicamente, Coil necesita dos cosas:

  • La URL de la imagen que quieres cargar y mostrar.
  • Un objeto ImageView para mostrar esa imagen.

En esta tarea, aprenderás a utilizar Coil para mostrar una sola imagen del servicio web de Marte. Debes mostrar la imagen de la primera foto de Marte en la lista de fotos que muestra el servicio web. Estas son las capturas de pantalla de antes y después:

Cómo agregar una dependencia de Coil

  1. Abre la app de MarsPhotos solution del codelab anterior.
  2. Ejecuta la app para ver qué hace. (Muestra la cantidad total de fotos de Marte obtenidas).
  3. Abre build.gradle (Module: app).
  4. En la sección dependencies, agrega esta línea para la biblioteca de Coil:
    // Coil
    implementation "io.coil-kt:coil:1.1.1"

Consulta la versión más reciente de la biblioteca y actualízala desde la página de documentación de Coil.

  1. La biblioteca de Coil está alojada y disponible en el repositorio mavenCentral(). En build.gradle (Project: MarsPhotos), agrega mavenCentral() al bloque repositories superior.
repositories {
   google()
   jcenter()
   mavenCentral()
}
  1. Haz clic en Sync Now para volver a compilar el proyecto con la dependencia nueva.

Cómo actualizar el ViewModel

En este paso, agregarás una propiedad LiveData a la clase OverviewViewModel para almacenar el objeto de Kotlin recibido, MarsPhoto.

  1. Abre overview/OverviewViewModel.kt. Debajo de la declaración de propiedad _status, agrega una nueva propiedad mutable llamada _photos, del tipo MutableLiveData, que pueda almacenar un solo objeto MarsPhoto.
private val _photos = MutableLiveData<MarsPhoto>()

Importa com.example.android.marsphotos.network.MarsPhoto cuando se solicite.

  1. Debajo de la declaración _photos, agrega un campo de copia de seguridad público llamado photos del tipo LiveData<MarsPhoto>.
val photos: LiveData<MarsPhoto> = _photos
  1. En el método getMarsPhotos(), dentro del bloque try{}, busca la línea que establece los datos recuperados del servicio web como listResult..
try {
   val listResult = MarsApi.retrofitService.getPhotos()
   ...
}
  1. Asigna la primera foto de Marte recuperada a la nueva variable _photos. Cambia listResult a _photos.value. Asigna la primera URL de las fotos al índice 0. Se mostrará un error; lo corregirás más tarde.
try {
   _photos.value = MarsApi.retrofitService.getPhotos()[0]
   ...
}
  1. En la siguiente línea, actualiza status.value a lo siguiente. Usa los datos de la propiedad nueva en lugar de listResult. Muestra la URL de la primera imagen de la lista de fotos.
try {
   ...
   _status.value = "   First Mars image URL : ${_photos.value!!.imgSrcUrl}"

}
  1. El bloque try{} completo ahora tiene el siguiente aspecto:
try {
   _photos.value = MarsApi.retrofitService.getPhotos()[0]
   _status.value = "   First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
  1. Ejecuta la app. Ahora TextView muestra la URL de la primera foto de Marte. Todo lo que hiciste hasta ahora es configurar ViewModel y LiveData para esa URL.

b8ac93805b69b03a.png

Cómo usar adaptadores de vinculación

Los adaptadores de vinculación son métodos anotados que se usan a fin de crear métodos set personalizados para propiedades personalizadas de la vista.

Por lo general, cuando estableces un atributo en el XML mediante el código android:text="Sample Text", el sistema Android busca automáticamente un método set con el mismo nombre que el atributo text, que se establece mediante el método setText(String: text). El método setText(String: text) es un método set para algunas vistas que proporciona el framework de Android. Un comportamiento similar se puede personalizar con los adaptadores de vinculación; puedes proporcionar un atributo personalizado y una lógica personalizada a la que llamará la biblioteca de vinculación de datos.

Ejemplo:

Para hacer algo más complejo que simplemente llamar a un método set en la vista de imagen, establece una imagen de elemento de diseño. Considera cargar imágenes fuera del subproceso de IU (subproceso principal) de Internet. Primero, elige un atributo personalizado para asignar la imagen a una ImageView. En el siguiente ejemplo, es imageUrl.

<ImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:imageUrl="@{product.imageUrl}"/>

Si no agregas ningún otro código, el sistema buscará un método setImageUrl(String) en ImageView y no lo encontrará, y verás un error porque este es un atributo personalizado que el framework no proporcionó. Debes crear una forma de implementar y establecer el atributo app:imageUrl en ImageView. Para ello, usarás adaptadores de vinculación (métodos anotados).

Ejemplo de un adaptador de vinculación:

@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        // Load the image in the background using Coil.
        }
    }
}

La anotación @BindingAdapter toma el nombre del atributo como su parámetro.

En el método bindImage, el primer parámetro del método es el tipo de la vista de destino, y el segundo es el valor que se establece en el atributo.

Dentro del método, la biblioteca Coil carga la imagen fuera del subproceso de la IU y la configura en ImageView.

Cómo crear un adaptador de vinculación y usar Coil

  1. En el paquete com.example.android.marsphotos, crea un archivo de Kotlin llamado BindingAdapters. Este archivo contendrá los adaptadores de vinculación que usas en toda la app.

a04afbd6ae8ccfcd.png

  1. In BindingAdapters.kt, crea una función bindImage() que tome un ImageView y un String como parámetros.
fun bindImage(imgView: ImageView, imgUrl: String?) {

}

Importa android.widget.ImageView cuando se solicite.

  1. Anota la función con @BindingAdapter. La anotación @BindingAdapter indica a la vinculación de datos que ejecute este adaptador de vinculación cuando un elemento View tiene el atributo imageUrl.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}

Importa androidx.databinding.BindingAdapter cuando se solicite.

función de alcance en let

let es una de las funciones Scope de Kotlin que te permite ejecutar un bloque de código dentro del contexto de un objeto. Hay cinco funciones de alcance en Kotlin. Consulta la documentación para obtener más información.

Uso:

let se usa para invocar una o más funciones en los resultados de las cadenas de llamadas.

La función let junto con un operador de llamada seguro (?.) se usa para realizar una operación segura nula en el objeto. En este caso, el bloque de código let solo se ejecutará si el objeto no es nulo.

  1. Dentro de la función bindImage(), agrega un bloque let{} al argumento imageURL mediante el operador de llamada segura.
imgUrl?.let {
}
  1. Dentro del bloque let{}, agrega la siguiente línea para convertir la string de URL en un objeto Uri con el método toUri(). Para usar el esquema HTTPS, agrega buildUpon.scheme("https") al compilador de toUri. Llama a build() para compilar el objeto.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()

Importa androidx.core.net.toUri cuando se solicite.

  1. Dentro del bloque let{}, después de la declaración imgUri, usa load(){} de Coil para cargar la imagen del objeto imgUri a imgView.
imgView.load(imgUri) {
}

Importa coil.load cuando se solicite.

  1. Tu método completo debe verse de la siguiente manera:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
        imgView.load(imgUri)
    }
}

Cómo actualizar el diseño y los fragmentos

En la sección anterior, usaste la biblioteca de imágenes de Coil para cargar la imagen. Para ver la imagen en la pantalla, el siguiente paso es actualizar la ImageView con el atributo nuevo para mostrar una imagen única.

Más adelante en el codelab, usarás res/layout/grid_view_item.xml como el archivo de recursos de diseño de cada elemento de la cuadrícula en RecyclerView. En esta tarea, usarás este archivo temporalmente para mostrar la imagen con la URL de la imagen que recuperaste en la tarea anterior. Usarás temporalmente este archivo de diseño en lugar de fragment_overview.xml.

  1. Abre res/layout/grid_view_item.xml.
  2. Por encima del elemento <ImageView>, agrega un elemento <data> para la vinculación de datos y realiza la vinculación a la clase OverviewViewModel:
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
  1. Agrega un atributo app:imageUrl al elemento ImageView para usar el nuevo adaptador de vinculación de carga de imágenes. Recuerda que el photos contiene una lista MarsPhotos recuperada del servidor. Asigna la primera URL de entrada al atributo imageUrl.
    <ImageView
        android:id="@+id/mars_image"
        ...
        app:imageUrl="@{viewModel.photos.imgSrcUrl}"
        ... />
  1. Abre overview/OverviewFragment.kt. En el método onCreateView(), comenta la línea que aumenta la clase FragmentOverviewBinding y la asigna a la variable de vinculación. Verás errores debido a la eliminación de esta línea. Son temporales; los corregirás más adelante.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. Usa grid_view_item.xml en lugar de fragment_overview.xml. Agrega la siguiente línea para aumentar la clase GridViewItemBinding en su lugar.
val binding = GridViewItemBinding.inflate(inflater)

Importa com.example.android.marsphotos. databinding.GridViewItemBinding si se te solicita.

  1. Ejecuta la app. Ahora deberías ver una sola imagen de Marte.

e59b6e849e63ae2b.png

Cómo agregar imágenes de carga y error

Usar Coil puede mejorar la experiencia del usuario, ya que muestra una imagen de marcador de posición mientras carga la imagen y una imagen de error si la carga falla, por ejemplo, si la imagen falta o está dañada. En este paso, agregarás esa funcionalidad al adaptador de vinculación.

  1. Abre res/drawable/ic_broken_image.xml y haz clic en la pestaña Design de la derecha. Para la imagen de error, utiliza el ícono de imagen rota que se encuentra disponible en la biblioteca de íconos integrada. Este elemento de diseño vectorial usa el atributo android:tint para colorear el ícono gris.

467c213c859e1904.png

  1. Abre res/drawable/loading_animation.xml. Este elemento de diseño es una animación que rota un elemento de diseño de imagen, loading_img.xml, alrededor del punto central. (No ves la animación en la vista previa).

6c1f87d1c932c762.png

  1. Regresa al archivo BindingAdapters.kt. En el método bindImage(), actualiza la llamada a imgView.load(imgUri) para agregar una lambda al final de la siguiente manera: este código establece la imagen de carga del marcador de posición para usarla durante la carga (elemento de diseño loading_animation). Este código también configura una imagen para usarla si falla la carga (elemento de diseño broken_image).
imgView.load(imgUri) {
   placeholder(R.drawable.loading_animation)
   error(R.drawable.ic_broken_image)
}
  1. El método bindImage() completo ahora tiene el siguiente aspecto:
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
    imgUrl?.let {
        val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
        imgView.load(imgUri) {
            placeholder(R.drawable.loading_animation)
            error(R.drawable.ic_broken_image)
        }
    }
}
  1. Ejecuta la app. Según la velocidad de la conexión de red, es posible que veas brevemente la imagen de carga mientras Glide descarga y muestra la imagen de la propiedad. Sin embargo, aún no verá el ícono de la imagen rota, incluso si desactiva la red; lo solucionarás en la última tarea del codelab.

80553d5e5c7641de.gif

  1. Revierte los cambios temporales que realizaste en overview/OverviewFragment.kt. En el método onCreateview(), quita el comentario de la línea que aumenta FragmentOverviewBinding. Borra o comenta la línea que aumenta GridViewIteMBinding.
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)

Ahora tu app carga una foto de Marte desde Internet. Con los datos del primer elemento de lista MarsPhoto, creaste una propiedad LiveData en ViewModel y utilizaste la URL de imagen de esos datos de fotos de Marte para propagar ImageView. Sin embargo, el objetivo es que la app muestre una cuadrícula de imágenes, así que en esta tarea usarás un RecyclerView con un administrador de diseño de cuadrícula para mostrar la cuadrícula de imágenes.

Cómo actualizar el ViewModel

En la tarea anterior, en el OverviewViewModel, agregaste un objeto LiveData llamado _photos que contiene un objeto MarsPhoto, es el primero en la lista de respuestas del servicio web. En este paso, cambiarás este LiveData para conservar la lista completa de objetos MarsPhoto.

  1. Abre overview/OverviewViewModel.kt.
  2. Cambia el tipo _photos para que sea una lista de objetos MarsPhoto.
private val _photos = MutableLiveData<List<MarsPhoto>>()
  1. Reemplaza el tipo de propiedad photos de copia de seguridad al tipo List<MarsPhoto> también:
 val photos: LiveData<List<MarsPhoto>> = _photos
  1. Desplázate hacia abajo hasta el bloque try {} dentro del método getMarsPhotos(). MarsApi.retrofitService.getPhotos()

muestra una lista de objetos MarsPhoto, que puedes asignar a _photos.value.

_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
  1. El bloque try/catch completo ahora tiene el siguiente aspecto:
try {
    _photos.value = MarsApi.retrofitService.getPhotos()
    _status.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
    _status.value = "Failure: ${e.message}"
}

GridLayout

El objeto GridLayoutManager de RecyclerView define los datos como una cuadrícula desplazable, como se muestra a continuación.

fcf0fc4b78f8650.png

Desde la perspectiva del diseño, el diseño de cuadrícula es ideal para listas que se pueden representar como imágenes o íconos, como listas dentro de la app de navegación de fotos de Marte.

Cómo GridLayout diseña los elementos

GridLayout organiza los elementos en una cuadrícula de filas y columnas. Suponiendo que el desplazamiento es vertical, de forma predeterminada, cada elemento de una fila ocupa un "intervalo". Un elemento puede ocupar varios intervalos. En el siguiente caso, un intervalo es equivalente al ancho de una columna, que es 3.

En los dos ejemplos que se muestran a continuación, cada fila está compuesta por tres intervalos. De forma predeterminada, el GridLayoutManager coloca cada elemento en un intervalo hasta el recuento de intervalos que especificas. Cuando alcanza el recuento de intervalos, se ajusta a la siguiente línea.

Cómo agregar Recyclerview

En este paso, modificarás el diseño de la app para usar una vista de reciclador con un diseño de cuadrícula, en lugar de la vista de imagen única.

  1. Abre layout/gridview_item.xml. Quita la variable de datos viewModel.
  2. Dentro de la etiqueta <data>, agrega la siguiente variable photo del tipo MarsPhoto.
<data>
   <variable
       name="photo"
       type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
  1. En <ImageView>, cambia el atributo app:imageUrl para hacer referencia a la URL de la imagen en el objeto MarsPhoto. Estos cambios revierten los cambios temporales que realizaste en la tarea anterior.
app:imageUrl="@{photo.imgSrcUrl}"
  1. Abre layout/fragment_overview.xml. Borra todo el elemento <TextView>
  2. En su lugar, agrega el siguiente elemento <RecyclerView>. Configura los atributos photos_grid, width y height para 0dp, de modo que ocupe el ConstraintLayout superior. Usarás un diseño de cuadrícula, por lo que debes establecer el atributo layoutManager en androidx.recyclerview.widget.GridLayoutManager. Establece un spanCount en 2 para que haya dos columnas.
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/photos_grid"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layoutManager=
       "androidx.recyclerview.widget.GridLayoutManager"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:spanCount="2" />
  1. Para obtener una vista previa de cómo se ve el código anterior en la vista Design, usa tools:itemCount a fin de configurar la cantidad de elementos que se muestran en nuestro diseño en 16. El atributo itemCount especifica la cantidad de elementos que el editor de diseño debe procesar en la ventana Preview. Establece el diseño de los elementos de la lista en grid_view_item con tools:listitem.
<androidx.recyclerview.widget.RecyclerView
            ...
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item" />
  1. Cambia a la vista Design, deberías ver una vista previa como la de la siguiente captura de pantalla. Esto no se ve como fotos de Marte, pero te mostrará cómo se verá el diseño de tu cuadrícula de Recyclerview. La vista previa usa el relleno y el diseño grid_view_item para cada elemento de cuadrícula de recyclerview.

20742824367c3952.png

  1. De acuerdo con los lineamientos de Material Design, debes tener 8dp de espacio en la parte superior, inferior y lateral de la lista, y 4dp de espacio entre los elementos. Puedes lograr esto con una combinación de relleno en el diseño fragment_overview.xml y en el diseño gridview_item.xml.

a3561fa85fea7a8f.png

  1. Abre layout/gridview_item.xml. Observa el atributo padding; ya tienes 2dp de relleno entre el exterior del elemento y el contenido. Eso nos dará 4dp del espacio entre el contenido del elemento y 2dp en los bordes externos, lo que significa que necesitaremos 6dp adicionales de relleno en los bordes externos para cumplir con los lineamientos de diseño.
  2. Vuelve a layout/fragment_overview.xml. Agrega 6dp de relleno para RecyclerView, por lo que tendrás 8dp en el exterior y 4dp en el interior, como en los lineamientos.
<androidx.recyclerview.widget.RecyclerView
            ...
            android:padding="6dp"
            ...  />
  1. El elemento <RecyclerView> completo debe verse de la siguiente manera.
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/photos_grid"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:padding="6dp"
    app:layoutManager=
        "androidx.recyclerview.widget.GridLayoutManager"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:spanCount="2"
    tools:itemCount="16"
    tools:listitem="@layout/grid_view_item"  />

Cómo agregar el adaptador de cuadrícula de fotos

Ahora el diseño fragment_overview tiene una RecyclerView con un diseño de cuadrícula. En este paso, vincularás los datos recuperados del servidor web a RecyclerView a través de un adaptador RecyclerView.

ListAdapter (actualizador)

ListAdapter es una subclase de la clase RecyclerView.Adapter para presentar datos de lista en un RecyclerView, incluidas las diferencias de cálculo entre listas en un subproceso en segundo plano.

En esta app, usarás la implementación DiffUtil en el ListAdapter.. La ventaja de usar DiffUtil es que, cada vez que se agrega, quita o cambia algún elemento de RecyclerView, no se actualiza la lista completa. Solo se actualizan los elementos que se modificaron.

Agrega ListAdapter a tu app.

  1. En el paquete overview, crea una nueva clase de Kotlin llamada PhotoGridAdapter.kt.
  2. Extiende la clase PhotoGridAdapter de ListAdapter con los parámetros del constructor que se muestran a continuación. La clase PhotoGridAdapter extiende ListAdapter, cuyo constructor necesita el tipo de elemento de lista, el contenedor de vistas y una implementación DiffUtil.ItemCallback.
class PhotoGridAdapter : ListAdapter<MarsPhoto,
        PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}

Importa las clases androidx.recyclerview.widget.ListAdapter y com.example.android.marsphoto.network.MarsPhoto si se te solicita. En los siguientes pasos, implementarás las otras implementaciones faltantes de este constructor que producen errores.

  1. Para solucionar los errores anteriores, agregarás los métodos necesarios en este paso y los implementarás más adelante en esta tarea. Haz clic en la clase PhotoGridAdapter, selecciona la bombilla roja y, en el menú desplegable, selecciona Implement members. En la ventana emergente que se muestra, selecciona los métodos ListAdapter, onCreateViewHolder() y onBindViewHolder(). Android Studio seguirá mostrando errores, que corregirás al final de esta tarea.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPhotoViewHolder {
   TODO("Not yet implemented")
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPhotoViewHolder, position: Int) {
   TODO("Not yet implemented")
}

Para implementar los métodos onCreateViewHolder y onBindViewHolder, necesitas MarsPhotoViewHolder, que agregarás en el siguiente paso.

  1. Dentro del PhotoGridAdapter, agrega una definición de clase interna para MarsPhotoViewHolder, que extiende RecyclerView.ViewHolder. Necesitas la variable GridViewItemBinding para vincular MarsPhoto al diseño, así que pasa la variable a MarsPhotoViewHolder. La clase base ViewHolder requiere una vista en su constructor, le pasas la vista raíz de vinculación.
class MarsPhotoViewHolder(private var binding:
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {
}

Importa androidx.recyclerview.widget.RecyclerView y com.example.android.marsrealestate.databinding.GridViewItemBinding si se te solicita.

  1. En MarsPhotoViewHolder, crea un método bind() que reciba un objeto MarsPhoto como argumento y establezca binding.property en ese objeto. Llama a executePendingBindings() después de configurar la propiedad, lo que hará que la actualización se ejecute de inmediato.
fun bind(MarsPhoto: MarsPhoto) {
   binding.photo = MarsPhoto
   binding.executePendingBindings()
}
  1. Dentro de la clase PhotoGridAdapter de onCreateViewHolder(), quita el comentario TODO y agrega la línea que se muestra a continuación. El método onCreateViewHolder() debe mostrar un MarsPhotoViewHolder nuevo, creado mediante el aumento del GridViewItemBinding y el uso de LayoutInflater de tu contexto de ViewGroup principal.
   return MarsPhotoViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))

Importa android.view.LayoutInflater si se te solicita.

  1. En el método onBindViewHolder(), quita el comentario TODO y agrega las líneas que se muestran a continuación. Aquí llamas a getItem() para obtener el objeto MarsPhoto asociado con la posición RecyclerView actual y, luego, pasas esa propiedad al método bind() en MarsPhotoViewHolder.
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
  1. Dentro de PhotoGridAdapter, agrega una definición de objeto complementario para DiffCallback, como se muestra a continuación.
    El objeto DiffCallback extiende DiffUtil.ItemCallback con el tipo genérico de objeto que deseas comparar: MarsPhoto. Vas a comparar dos objetos de fotos de Marte dentro de la implementación.
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
}

Importa androidx.recyclerview.widget.DiffUtil cuando se solicite.

  1. Presiona la bombilla roja para implementar los métodos del comparador del objeto DiffCallback, que son areItemsTheSame() y areContentsTheSame().
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   TODO("Not yet implemented")
}

override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   TODO("Not yet implemented") }
  1. En el método areItemsTheSame(), quita el TODO. DiffUtil llama a este método para decidir si dos objetos representan el mismo elemento. DiffUtil usa este método para determinar si el objeto MarsPhoto nuevo es el mismo que el objeto MarsPhoto anterior. El ID de cada elemento (objetoMarsPhoto) es único. Compara los ID de oldItem y newItem, y muestra el resultado.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   return oldItem.id == newItem.id
}
  1. En areContentsTheSame(), quita el TODO. DiffUtil llama a este método cuando desea verificar si dos elementos tienen los mismos datos. Los datos importantes de MarsPhoto son la URL de la imagen. Compara las URL de oldItem y newItem, y muestra el resultado.
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   return oldItem.imgSrcUrl == newItem.imgSrcUrl
}

Asegúrate de poder compilar y ejecutar la app sin errores, pero el emulador mostrará una pantalla en blanco. Tienes el Recyclerview listo, pero no se le pasaron datos, lo que implementarás en el siguiente paso.

Cómo agregar el adaptador de vinculación y conectar las partes

En este paso, usarás un elemento BindingAdapter para inicializar PhotoGridAdapter con la lista de objetos MarsPhoto. Si usas un BindingAdapter para configurar los datos de RecyclerView, la vinculación de datos observa automáticamente el LiveData para la lista de objetos MarsPhoto. Luego, se llama al adaptador de vinculación automáticamente cuando cambia la lista MarsPhoto.

  1. Abre BindingAdapters.kt.
  2. Al final del archivo, agrega un método bindRecyclerView() que tome un RecyclerView y una lista de objetos MarsPhoto como argumentos. Anota ese método con @BindingAdapter con el atributo listData.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
    data: List<MarsPhoto>?) {
}

Importa androidx.recyclerview.widget.RecyclerView y com.example.android.marsphotos.network.MarsPhoto si se te solicita.

  1. Dentro de la función bindRecyclerView(), transmite recyclerView.adapter a PhotoGridAdapter y asígnalo a un nuevo adapter. de la propiedad val.
val adapter = recyclerView.adapter as PhotoGridAdapter
  1. Al final de la función bindRecyclerView(), llama a adapter.submitList() con los datos de la lista de fotos de Marte. Esto le indica a RecyclerView cuándo hay una lista nueva disponible.
adapter.submitList(data)

Importa com.example.android.marsrealestate.overview.PhotoGridAdapter si se te solicita.

  1. El adaptador de vinculación bindRecyclerView completo debe verse de la siguiente manera:
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
                    data: List<MarsPhoto>?) {
   val adapter = recyclerView.adapter as PhotoGridAdapter
   adapter.submitList(data)

}
  1. Para conectar todo, abre res/layout/fragment_overview.xml. Agrega el atributo app:listData al elemento RecyclerView y establécelo en viewmodel.photos con la vinculación de datos. Esto es similar a lo que hiciste para ImageView en una tarea anterior.
app:listData="@{viewModel.photos}"
  1. Abre overview/OverviewFragment.kt. En onCreateView(), justo antes de la sentencia return, inicializa el adaptador RecyclerView en binding.photosGrid en un objeto PhotoGridAdapter nuevo.
binding.photosGrid.adapter = PhotoGridAdapter()
  1. Ejecuta la app. Deberías ver una cuadrícula de imágenes desplazables de Marte. Mientras te desplazas para ver imágenes nuevas, notas que el aspecto es extraño El relleno permanece en la parte superior e inferior de RecyclerView a medida que te desplazas, por lo que nunca parece que te desplazas por la lista en la barra de acciones.

5d03641aa1589842.png

  1. Para solucionar este problema, debes indicar a RecyclerView que no recorte el contenido interno al relleno mediante el atributo android:clipToPadding. De este modo, se dibuja la vista de desplazamiento en el área de relleno. Vuelve a layout/fragment_overview.xml. Agrega el atributo android:clipToPadding para el RecyclerView y configúralo como false.
<androidx.recyclerview.widget.RecyclerView
            ...
            android:clipToPadding="false"
            ...  />
  1. Ejecuta la app. Observa que esta también muestra el ícono de progreso de carga antes de mostrar la imagen, según lo previsto. Esta es la imagen de carga del marcador de posición que pasaste a la biblioteca de imágenes de Coil.

3128b84aa22ef97e.png

  1. Mientras se ejecuta la app, activa el modo de avión. Desplázate por las imágenes en el emulador. Las imágenes que aún no se cargaron se muestran como íconos de imágenes rotas. Este es el elemento de diseño de imagen que pasaste a la biblioteca de imágenes de Coil para mostrar en caso de que no se obtenga el error o la imagen de red.

28d2cbba564f35ff.png

Felicitaciones. Ya casi terminas. En la próxima tarea final, aumentarás el manejo de errores en la app para mejorar la experiencia del usuario.

La app de MarsPhotos muestra el ícono de la imagen rota cuando no se puede recuperar una imagen. Sin embargo, cuando no dispones de una red, la app muestra una pantalla en blanco. Verificarás la pantalla en blanco en el paso siguiente.

  1. Activa el modo de avión en el dispositivo o el emulador. Ejecuta la app desde Android Studio. Observa la pantalla en blanco.

492011786c2dd7f7.png

Esta experiencia del usuario no es buena. En esta tarea, agregarás una administración de errores básica para que el usuario tenga una idea más clara de lo que sucede. Si Internet no está disponible, la app mostrará el ícono de error de conexión y, mientras recupera la lista MarsPhoto, la app mostrará la animación de carga.

Cómo agregar estado al ViewModel

En esta tarea, crearás una propiedad en OverviewViewModel para representar el estado de la solicitud web. Hay tres estados que se deben considerar: carga, éxito y error. El estado de carga se produce mientras esperas los datos. El estado de éxito es cuando se recuperan correctamente los datos del servicio web. El estado de error indica cualquier error de red o conexión.

Clases enum en Kotlin

Para representar estos tres estados en tu aplicación, usarás enum. enum es la abreviatura de enumeración, lo que significa una lista ordenada de todos los elementos de una colección. Cada constante enum es un objeto de la clase enum.

En Kotlin, un enum es un tipo de datos que puede contener un conjunto de constantes. Se definen al agregar la palabra clave enum delante de una definición de clase, como se muestra a continuación. Las constantes de enum se separan con comas.

Definición:

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

Uso:

var direction = Direction.NORTH;

Como se muestra arriba, se puede hacer referencia a los objetos enum usando el nombre de clase seguido de un operador punto (.) y el nombre de la constante.

Agrega la definición de clase enum con los valores de estado en el Viewmodel.

  1. Abre overview/OverviewViewModel.kt. En la parte superior del archivo (después de las importaciones, antes de la definición de clase), agrega un enum para representar todos los estados disponibles:
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. Desplázate hasta la definición de las propiedades _status y status. Cambia los tipos de String a MarsApiStatus. MarsApiStatus como la clase de enum que definiste en el paso anterior.
private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus> = _status
  1. En el método getMarsPhotos(), cambia la string "Success: ..." al estado MarsApiStatus.DONE y la string "Failure..." a MarsApiStatus.ERROR.
try {
    _photos.value = MarsApi.retrofitService.getPhotos()
    _status.value = MarsApiStatus.DONE
} catch (e: Exception)
     _status.value = MarsApiStatus.ERROR
}
  1. Establece el estado en MarsApiStatus.LOADING sobre el bloque try {}. Es el estado inicial mientras se ejecuta la corrutina y esperas los datos. El bloque viewModelScope.launch {} completo ahora tiene el siguiente aspecto:
viewModelScope.launch {
            _status.value = MarsApiStatus.LOADING
            try {
                _photos.value = MarsApi.retrofitService.getPhotos()
                _status.value = MarsApiStatus.DONE
            } catch (e: Exception) {
                _status.value = MarsApiStatus.ERROR
            }
        }
  1. Después del estado de error en el bloque catch {}, establece _photos en una lista vacía. Esta acción borrará la vista de reciclador.
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _photos.value = listOf()
}
  1. El método getMarsPhotos() completo debería verse de la siguiente manera:
private fun getMarsPhotos() {
   viewModelScope.launch {
        _status.value = MarsApiStatus.LOADING
        try {
           _photos.value = MarsApi.retrofitService.getPhotos()
           _status.value = MarsApiStatus.DONE
        } catch (e: Exception) {
           _status.value = MarsApiStatus.ERROR
           _photos.value = listOf()
        }
    }
}

Definiste los estados de enum del estado y estableciste el estado de carga al comienzo de la corrutina. Indica Done cuando tu app haya terminado de recuperar los datos del servidor web e indica Error cuando haya una excepción. En la siguiente tarea, usará un adaptador de vinculación para mostrar los íconos correspondientes.

Cómo agregar un adaptador de vinculación para el estado ImageView

Configuraste MarsApiStatus en OverviewViewModel con un conjunto de estados de enum. En este paso, harás que aparezca en la app. Usa un adaptador de vinculación para un ImageView a fin de mostrar íconos para los estados de carga y error. Cuando la app se encuentre en el estado de carga o de error, ImageView debería estar visible. Cuando la app termine de cargarse, el ImageView debería estar invisible.

  1. Para agregar otro adaptador, abre BindingAdapters.kt y desplázate hasta el final del archivo. Agrega un nuevo adaptador de vinculación llamado bindStatus() que toma un valor ImageView y un valor MarsApiStatus como argumentos. Anota el método con @BindingAdapter y pasa el atributo personalizado marsApiStatus como parámetro.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
          status: MarsApiStatus?) {
}

Importa com.example.android.marsrealestate.overview.MarsApiStatus si se te solicita.

  1. Agrega un bloque when {} dentro del método bindStatus() para alternar entre los diferentes estados.
when (status) {

}
  1. Dentro del when {}, agrega un caso para el estado de carga (MarsApiStatus.LOADING). Para este estado, configura el ImageView como visible y asígnale la animación de carga. Este es el mismo elemento de diseño de animación que usaste para Coil en la tarea anterior.
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}

Importa android.view.View si se te solicita.

  1. Agrega un caso para el estado Error, que es MarsApiStatus.ERROR. De manera similar a lo que hiciste para el estado LOADING, configura el estado ImageView como visible y usa el elemento de diseño de error de conexión.
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. Agrega un caso para el estado Done, que es MarsApiStatus.DONE. Aquí tienes una respuesta correcta, así que configura el nivel de visibilidad del estado ImageView como View.GONE para ocultarlo.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

Configuraste el adaptador de vinculación para la vista de imagen de estado. En el siguiente paso, agregarás una vista de imagen que use el nuevo adaptador de vinculación.

Cómo agregar el estado ImageView

En este paso, agregarás la vista de imagen en el fragment_overview.xml que mostrará el estado que definiste antes.

  1. Abre res/layout/fragment_overview.xml. Dentro de ConstraintLayout, debajo del elemento RecyclerView, agrega la ImageView que se muestra a continuación.
<ImageView
   android:id="@+id/status_image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:marsApiStatus="@{viewModel.status}" />

La ImageView anterior tiene las mismas restricciones que RecyclerView. Sin embargo, el ancho y el alto usan wrap_content para centrar la imagen en lugar de estirarla para llenar la vista. Observa también el atributo app:marsApiStatus establecido en viewModel.status, que llama a tu BindingAdapter cuando la propiedad de estado de ViewModel cambia.

  1. Para probar el código anterior, simula el error de conexión de red activando el modo de avión del dispositivo o emulador. Compila y ejecuta la app, y observa la imagen de error que aparece:

a91ddb1c89f2efec.png

  1. Presiona el botón Back para cerrar la app y desactiva el modo de avión. Usa la pantalla de recientes para regresar a la app. Según la velocidad de tu conexión de red, es posible que veas un ícono giratorio de carga extremadamente breve cuando la app consulte el servicio web antes de que las imágenes comiencen a cargarse.

Felicitaciones por completar este codelab y crear la app de MarsPhotos. Es hora de que alardees de tu app con fotos reales de Marte con tu familia y amigos.

El código de solución para este codelab se encuentra en el proyecto que se muestra a continuación. Usa la rama main para extraer o descargar el código.

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. En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.

Eme2bJP46u-pMpnXVfm-bS2N2dlyq6c0jn1DtQYqBaml7TUhzXDWpYoDI0lGKi4xndE_uJw8sKfwfOZ1fC503xCVZrbh10JKJ4iEHdLDwFfdvnOheNxkokITW1LW6UZTncVJJUZ5Fw

  1. En el cuadro de diálogo, 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 an existing Android Studio project.

Tdjf5eS2nCikM9KdHgFaZNSbIUCzKXP6WfEaKVE2Oz1XIGZhgTJYlaNtXTHPFU1xC9pPiaD-XOPdIxVxwZAK8onA7eJyCXz2Km24B_8rpEVI_Po5qlcMNN8s4Tkt6kHEXdLQTDW7mg

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

PaMkVnfCxQqSNB1LxPpC6C6cuVCAc8jWNZCqy5tDVA6IO3NE2fqrfJ6p6ggGpk7jd27ybXaWU7rGNOFi6CvtMyHtWdhNzdAHmndzvEdwshF_SG24Le01z7925JsFa47qa-Q19t3RxQ

  1. En el cuadro de diálogo Import Project, navega 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 j7ptomO2PEQNe8jFt4nKCOw_Oc_Aucgf4l_La8fGLCMLy0t9RN9SkmBFGOFjkEzlX4ce2w2NWq4J30sDaxEe4MaSNuJPpMgHxnsRYoBtIV3-GUpYYcIvRJ2HrqR27XGuTS4F7lKCzg para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
  5. Explora los archivos del proyecto en la ventana de herramientas Project para ver cómo se implementó la app.
  • La biblioteca de Coil simplifica el proceso de administración de imágenes, como la descarga, el almacenamiento en búfer, la decodificación y el almacenamiento en caché de imágenes en tu app.
  • Los adaptadores de vinculación son métodos de extensión que se encuentran entre una vista y los datos vinculados de esa vista. Los adaptadores de vinculación proporcionan un comportamiento personalizado cuando cambian los datos, por ejemplo, para llamar a Coil a fin de cargar una imagen desde una URL en un ImageView.
  • Los adaptadores de vinculación son métodos de extensión anotados con la anotación @BindingAdapter.
  • Para mostrar una cuadrícula de imágenes, usa un RecyclerView con un GridLayoutManager.
  • Para actualizar la lista de propiedades cuando cambie, usa un adaptador de vinculación entre RecyclerView y el diseño.

Documentación para desarrolladores de Android:

Otra: