Aspectos básicos de Android Kotlin 08.2: Cómo cargar y mostrar imágenes de Internet

Este codelab es parte del curso Conceptos básicos de Kotlin para Android. Aprovecharás al máximo este curso si trabajas con los codelabs en secuencia. Todos los codelabs del curso se detallan en la página de destino de codelabs sobre los aspectos básicos de Kotlin para Android.

Introducción

En el codelab anterior, aprendiste a obtener datos de un servicio web y analizar la respuesta en un objeto de datos. 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.

Conocimientos que ya deberías tener

  • Cómo crear y usar fragmentos
  • Cómo usar componentes de arquitectura, incluidos modelos de vista, fábricas de modelos de vista, transformaciones y LiveData
  • 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 Glide 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

Actividades

  • Modifica la app de MarsRealEstate para obtener la URL de la imagen de los datos de la propiedad de Marte y usa Glide a fin de cargar y mostrar esa imagen.
  • Agregarás una animación de carga y un ícono de error a la app.
  • Usa un RecyclerView para mostrar una cuadrícula de imágenes de propiedades de Marte.
  • Agregarás administración de estado y errores a RecyclerView.

En este codelab (y codelabs relacionados), trabajarás con una app llamada MarsRealEstate, que muestra las propiedades a la venta en Marte. La app se conecta a un servidor de Internet para recuperar y mostrar datos de la propiedad, incluidos detalles como la propiedad y si se puede vender o alquilar. Las imágenes de cada propiedad son fotos reales de Marte capturadas por los rovers de la NASA.

La versión de la app que compilas en este codelab completará la página de descripción general, la cual muestra una cuadrícula de imágenes. Las imágenes son parte de los datos de propiedad que tu app obtiene del servicio web de bienes raíces de Marte. Tu app usará la biblioteca Glide 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.

Mostrar una foto de una URL web puede parecer sencillo, pero se necesita un poco de ingeniería para que funcione bien. La imagen debe descargarse, almacenarse en búfer y decodificarse de su formato comprimido a una imagen que Android pueda utilizar. 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. Aprender a cargar imágenes de forma efectiva desde la red podría ser un codelab en sí mismo.

Afortunadamente, puedes usar una biblioteca desarrollada por la comunidad llamada Glide para descargar, almacenar en búfer, decodificar y almacenar en caché tus imágenes. Glide te deja mucho menos trabajo que si tuvieras que hacer todo esto desde cero.

Básicamente, Glide necesita dos cosas:

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

En esta tarea, aprenderás a usar Glide para mostrar una sola imagen del servicio web de bienes raíces. Debes mostrar la imagen que representa la primera propiedad de Marte en la lista de propiedades que muestra el servicio web. Estas son las capturas de pantalla de antes y después:

Paso 1: Agrega la dependencia de Glide

  1. Abre la app de MarsRealEstate del último codelab. (Puedes descargar MarsRealEstateNetwork aquí si no tienes la app).
  2. Ejecuta la app para ver qué hace. (Muestra detalles de texto de una propiedad que está hipotéticamente disponible en Marte).
  3. Abre build.gradle (Module: app).
  4. En la sección dependencies, agrega esta línea para la biblioteca de Glide:
implementation "com.github.bumptech.glide:glide:$version_glide"


Observa que el número de versión ya está definido por separado en el archivo de Gradle del proyecto.

  1. Haz clic en Sync Now para volver a compilar el proyecto con la dependencia nueva.

Paso 2: Actualiza el modelo de vista

A continuación, actualizarás la clase OverviewViewModel para incluir datos en vivo de una sola propiedad de Marte.

  1. Abre overview/OverviewViewModel.kt. Debajo del LiveData de la _response, agrega datos en vivo internos (mutables) y externos (inmutables) para un solo objeto MarsProperty.

    Importa la clase MarsProperty (com.example.android.marsrealestate.network.MarsProperty) cuando se solicite.
private val _property = MutableLiveData<MarsProperty>()

val property: LiveData<MarsProperty>
   get() = _property
  1. En el método getMarsRealEstateProperties(), busca la línea dentro del bloque try/catch {} que establece _response.value en la cantidad de propiedades. Agrega la prueba que se muestra a continuación. Si los objetos MarsProperty están disponibles, esta prueba establece el valor de _property LiveData en la primera propiedad en listResult.
if (listResult.size > 0) {   
    _property.value = listResult[0]
}

El bloque try/catch {} completo ahora tiene el siguiente aspecto:

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   if (listResult.size > 0) {      
       _property.value = listResult[0]
   }
 } catch (e: Exception) {
    _response.value = "Failure: ${e.message}"
 }
  1. Abre el archivo res/layout/fragment_overview.xml. En el elemento <TextView>, cambia android:text para vincular al componente imgSrcUrl de property LiveData:
android:text="@{viewModel.property.imgSrcUrl}"
  1. Ejecuta la app. TextView solo muestra la URL de la imagen en la primera propiedad de Marte. Todo lo que hiciste hasta ahora es configurar el modelo de vista y los datos en vivo para esa URL.

Paso 3: Crea un adaptador de vinculación y llama a Glide

Ahora tienes la URL de una imagen para mostrar y es hora de comenzar a trabajar con Glide para cargar esa imagen. En este paso, usarás un adaptador de vinculación para tomar la URL de un atributo XML asociado con un ImageView y usarás la herramienta Glide para cargar la imagen. Los adaptadores de vinculación son métodos de extensión que se encuentran entre una vista y datos vinculados para proporcionar un comportamiento personalizado cuando los datos cambian. En este caso, el comportamiento personalizado es llamar a Glide para cargar una imagen desde una URL en ImageView.

  1. Abre BindingAdapters.kt. Este archivo contendrá los adaptadores de vinculación que usas en toda la app.
  2. Crea una función bindImage() que tome un ImageView y un String como parámetros. Anota la función con @BindingAdapter. La anotación @BindingAdapter indica a la vinculación de datos que deseas que este adaptador de vinculación se ejecute cuando un elemento XML tenga el atributo imageUrl.

    Importa androidx.databinding.BindingAdapter y android.widget.ImageView cuando se te solicite.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {

}
  1. Dentro de la función bindImage(), agrega un bloque let {} para el argumento imgUrl:
imgUrl?.let { 
}
  1. Dentro del bloque let {}, agrega la línea que se muestra a continuación para convertir la string de URL (del XML) en un objeto Uri. Importa androidx.core.net.toUri cuando se solicite.

    Quieres que el objeto Uri final use el esquema HTTPS, ya que el servidor del que extraes las imágenes requiere ese esquema. Para usar el esquema HTTPS, agrega buildUpon.scheme("https") al compilador de toUri. El método toUri() es una función de extensión de Kotlin de la biblioteca principal de Android KTX, por lo que parece ser parte de la clase String.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. Dentro de let {}, llama a Glide.with() para cargar la imagen del objeto Uri a ImageView. Importa com.bumptech.glide.Glide cuando se solicite.
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

Paso 4: Actualiza el diseño y los fragmentos

A pesar de que Glide cargó la imagen, todavía no hay nada para ver. El siguiente paso es actualizar el diseño y los fragmentos con un ImageView para mostrar la imagen.

  1. Abre res/layout/gridview_item.xml. Este es el archivo de recursos de diseño que usarás para cada elemento de RecyclerView más adelante en el codelab. Se usa temporalmente para mostrar solo la imagen.
  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.marsrealestate.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:
app:imageUrl="@{viewModel.property.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. Esto es solo temporal; volverás a verlo más tarde.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. En su lugar, agrega una línea para aumentar la clase GridViewItemBinding. Importa com.example.android.marsrealestate. databinding.GridViewItemBinding cuando se solicite.
val binding = GridViewItemBinding.inflate(inflater)
  1. Ejecuta la app. Ahora deberías ver una foto de la primera imagen de la lista MarsProperty de resultados de la lista.

Paso 5: Agrega imágenes de error y carga simples

Glide puede mejorar la experiencia del usuario mostrando una imagen de marcador de posición mientras se carga la imagen y una imagen de error si falla la carga (por ejemplo, si falta o está dañada). En este paso, agregarás esa funcionalidad al adaptador de vinculación y al diseño.

  1. Abre res/drawable/ic_broken_image.xml y haz clic en la pestaña Preview 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.

  1. Abre res/drawable/loading_animation.xml. Este elemento de diseño es una animación que se define con la etiqueta <animate-rotate>. La animación rota un elemento de diseño de imagen, loading_img.xml, alrededor del punto central. (La animación no se ve en la vista previa).

  1. Regresa al archivo BindingAdapters.kt. En el método bindImage(), actualiza la llamada a Glide.with() para llamar a la función apply() entre load() y into(). Importa com.bumptech.glide.request.RequestOptions cuando se solicite.

    Este código configura la imagen de carga del marcador de posición para usarla durante la carga (elemento de diseño loading_animation). El código también configura una imagen para usarla si falla la carga (elemento de diseño broken_image). 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()
        Glide.with(imgView.context)
                .load(imgUri)
                .apply(RequestOptions()
                        .placeholder(R.drawable.loading_animation)
                        .error(R.drawable.ic_broken_image))
                .into(imgView)
    }
}
  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ás el ícono de la imagen rota, incluso si desactivas la red; debes solucionarlo en la última parte del codelab.

Ahora tu app carga información de propiedades desde Internet. Con los datos del primer elemento de lista MarsProperty, creaste una propiedad LiveData en el modelo de vista y usaste la URL de la imagen de esos datos de propiedad para propagar un ImageView. Sin embargo, el objetivo es que tu app muestre una cuadrícula de imágenes, por lo que quieres usar una RecyclerView con una GridLayoutManager.

Paso 1: Actualiza el modelo de vista

En este momento, el modelo de vista tiene un _property LiveData que contiene un objeto MarsProperty, el primero de la lista de respuestas del servicio web. En este paso, cambiarás ese LiveData para conservar la lista completa de objetos MarsProperty.

  1. Abre overview/OverviewViewModel.kt.
  2. Cambia la variable privada _property a _properties. Cambia el tipo para que sea una lista de objetos MarsProperty.
private val _properties = MutableLiveData<List<MarsProperty>>()
  1. Reemplaza los datos activos property externos por properties. Agrega también la lista al tipo LiveData:
 val properties: LiveData<List<MarsProperty>>
        get() = _properties
  1. Desplázate hacia abajo hasta el método getMarsRealEstateProperties(). Dentro del bloque try {}, reemplaza toda la prueba que agregaste en la tarea anterior por la línea que se muestra a continuación. Debido a que la variable listResult contiene una lista de objetos MarsProperty, puedes asignarla a _properties.value en lugar de probar una respuesta correcta.
_properties.value = listResult

El bloque try/catch completo ahora tiene el siguiente aspecto:

try {
   var listResult = getPropertiesDeferred.await()
   _response.value = "Success: ${listResult.size} Mars properties retrieved"
   _properties.value = listResult
} catch (e: Exception) {
   _response.value = "Failure: ${e.message}"
}

Paso 2: Actualiza los diseños y fragmentos

El siguiente paso es cambiar el diseño y los fragmentos de la app para usar una vista de reciclador y un diseño de cuadrícula, en lugar de la vista de imagen única.

  1. Abre res/layout/gridview_item.xml. Cambia la vinculación de datos de OverviewViewModel a MarsProperty y cambia el nombre de la variable a "property".
<variable
   name="property"
   type="com.example.android.marsrealestate.network.MarsProperty" />
  1. En <ImageView>, cambia el atributo app:imageUrl para hacer referencia a la URL de la imagen en el objeto MarsProperty:
app:imageUrl="@{property.imgSrcUrl}"
  1. Abre overview/OverviewFragment.kt. En onCreateview(), quita el comentario de la línea que aumenta FragmentOverviewBinding. Borra o comenta la línea que aumenta GridViewBinding. Estos cambios revierten los cambios temporales que realizaste en la última tarea.
val binding = FragmentOverviewBinding.inflate(inflater)
 // val binding = GridViewItemBinding.inflate(inflater)
  1. Abre res/layout/fragment_overview.xml. Borra todo el elemento <TextView>
  2. En su lugar, agrega este elemento <RecyclerView>, que usa un diseño GridLayoutManager y grid_view_item para un solo elemento:
<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/photos_grid"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="6dp"
            android:clipToPadding="false"
            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" />

Paso 3: Agrega el adaptador de cuadrícula de fotos

Ahora, el diseño de fragment_overview tiene un RecyclerView, mientras que el de grid_view_item tiene una sola ImageView. En este paso, vinculas los datos a RecyclerView a través de un adaptador RecyclerView.

  1. Abre overview/PhotoGridAdapter.kt.
  2. Crea la clase PhotoGridAdapter, 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.

    Importa las clases androidx.recyclerview.widget.ListAdapter y com.example.android.marsrealestate.network.MarsProperty cuando se solicite. En los siguientes pasos, implementarás las otras partes faltantes de este constructor que producen errores.
class PhotoGridAdapter : ListAdapter<MarsProperty,
        PhotoGridAdapter.MarsPropertyViewHolder>(DiffCallback) {

}
  1. Haz clic en cualquier parte de la clase PhotoGridAdapter y presiona Control+i para implementar los métodos ListAdapter, que son onCreateViewHolder() y onBindViewHolder().
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPropertyViewHolder {
   TODO("not implemented") 
}

override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPropertyViewHolder, position: Int) {
   TODO("not implemented") 
}
  1. Al final de la definición de la clase PhotoGridAdapter, después de los métodos que acabas de agregar, agrega una definición de objeto complementario para DiffCallback, como se muestra a continuación.

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

    El objeto DiffCallback extiende DiffUtil.ItemCallback con el tipo de objeto que deseas comparar: MarsProperty.
companion object DiffCallback : DiffUtil.ItemCallback<MarsProperty>() {
}
  1. Presiona Control+i a fin de implementar los métodos del comparador para este objeto, que son areItemsTheSame() y areContentsTheSame().
override fun areItemsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") 
}

override fun areContentsTheSame(oldItem: MarsProperty, newItem: MarsProperty): Boolean {
   TODO("not implemented") }
  1. Para el método areItemsTheSame(), quita el comentario TODO. Usa el operador de igualdad referencial de Kotlin (===), que muestra true si las referencias de objetos para oldItem y newItem son iguales.
override fun areItemsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem === newItem
}
  1. Para areContentsTheSame(), usa el operador de igualdad estándar solo en el ID de oldItem y newItem.
override fun areContentsTheSame(oldItem: MarsProperty, 
                  newItem: MarsProperty): Boolean {
   return oldItem.id == newItem.id
}
  1. Dentro de la clase PhotoGridAdapter, debajo del objeto complementario, agrega una definición de clase interna para MarsPropertyViewHolder, que extiende RecyclerView.ViewHolder.

    Importa androidx.recyclerview.widget.RecyclerView y com.example.android.marsrealestate.databinding.GridViewItemBinding cuando se solicite.

    Necesitas la variable GridViewItemBinding para vincular MarsProperty al diseño, así que pasa la variable a MarsPropertyViewHolder. Como la clase base ViewHolder requiere una vista en su constructor, le pasas la vista raíz de vinculación.
class MarsPropertyViewHolder(private var binding: 
                   GridViewItemBinding):
       RecyclerView.ViewHolder(binding.root) {

}
  1. En MarsPropertyViewHolder, crea un método bind() que reciba un objeto MarsProperty 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(marsProperty: MarsProperty) {
   binding.property = marsProperty
   binding.executePendingBindings()
}
  1. En onCreateViewHolder(), quita el comentario TODO y agrega la línea que se muestra a continuación. Importa android.view.LayoutInflater cuando se solicite.

    El método onCreateViewHolder() debe mostrar un nuevo MarsPropertyViewHolder, creado mediante el aumento del GridViewItemBinding y el uso del LayoutInflater de tu contexto superior ViewGroup.
   return MarsPropertyViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))
  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 MarsProperty asociado con la posición actual RecyclerView y, luego, pasas esa propiedad al método bind() en MarsPropertyViewHolder.
val marsProperty = getItem(position)
holder.bind(marsProperty)

Paso 4: Agrega el adaptador de vinculación y conecta las partes

Por último, usa un BindingAdapter para inicializar el PhotoGridAdapter con la lista de objetos MarsProperty. 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 MarsProperty. Luego, se llama al adaptador de vinculación automáticamente cuando cambia la lista MarsProperty.

  1. Abre BindingAdapters.kt.
  2. Al final del archivo, agrega un método bindRecyclerView() que tome un RecyclerView y una lista de objetos MarsProperty como argumentos. Anota ese método con @BindingAdapter.

    Importa androidx.recyclerview.widget.RecyclerView y com.example.android.marsrealestate.network.MarsProperty cuando se te solicite.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, 
    data: List<MarsProperty>?) {
}
  1. Dentro de la función bindRecyclerView(), transmite recyclerView.adapter a PhotoGridAdapter y llama a adapter.submitList() con los datos. Esto le indica a RecyclerView cuándo hay una lista nueva disponible.

Importa com.example.android.marsrealestate.overview.PhotoGridAdapter cuando se solicite.

val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
  1. Abre res/layout/fragment_overview.xml. Agrega el atributo app:listData al elemento RecyclerView y establécelo en viewmodel.properties con la vinculación de datos.
app:listData="@{viewModel.properties}"
  1. Abre overview/OverviewFragment.kt. En onCreateView(), justo antes de la llamada a setHasOptionsMenu(), 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 MarsProperty imágenes. Mientras te desplazas para ver imágenes nuevas, la app muestra el ícono de progreso de carga antes de mostrar la imagen. Si activas el modo de avión, las imágenes que aún no se hayan cargado aparecerán como íconos de imágenes rotas.

La app de MarsRealEstate 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.

Esta no es una gran experiencia del usuario. En esta tarea, agregarás administración básica de errores para darle al usuario una mejor idea de lo que sucede. Si Internet no está disponible, la app mostrará el ícono de error de conexión. Mientras la app recupera la lista MarsProperty, mostrará la animación de carga.

Paso 1: Agrega el estado al modelo de vista

Para comenzar, crea un LiveData en el modelo de vista que represente 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 en la llamada a await().

  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. Cambia el nombre de las definiciones de datos en vivo internas y externas de _response a lo largo de la clase OverviewViewModel a _status. Como agregaste compatibilidad con el LiveData de _properties anteriormente en este codelab, no se usó la respuesta completa del servicio web. Aquí, necesitas una LiveData para hacer un seguimiento del estado actual a fin de poder cambiar el nombre de las variables existentes.

Además, cambia los tipos de String a MarsApiStatus.

private val _status = MutableLiveData<MarsApiStatus>()

val status: LiveData<MarsApiStatus>
   get() = _status
  1. Desplázate hacia abajo hasta el método getMarsRealEstateProperties() y actualiza _response a _status también aquí. Cambia la string "Success" al estado MarsApiStatus.DONE y la string "Failure" a MarsApiStatus.ERROR.
  2. Agrega un estado MarsApiStatus.LOADING a la parte superior del bloque try {}, antes de la llamada a await(). Es el estado inicial mientras se ejecuta la corrutina y esperas los datos. El bloque try/catch {} completo ahora tiene el siguiente aspecto:
try {
    _status.value = MarsApiStatus.LOADING
   var listResult = getPropertiesDeferred.await()
   _status.value = MarsApiStatus.DONE
   _properties.value = listResult
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
}
  1. Después del estado de error en el bloque catch {}, establece _properties LiveData en una lista vacía. Esta acción borrará el RecyclerView.
} catch (e: Exception) {
   _status.value = MarsApiStatus.ERROR
   _properties.value = ArrayList()
}

Paso 2: Agrega un adaptador de vinculación para el estado ImageView

Ahora tienes un estado en el modelo de vista, pero es solo un conjunto de estados. ¿Cómo puedes hacer que aparezca en la misma app? En este paso, usarás un elemento ImageView, conectado a la vinculación de datos, para mostrar íconos de los estados de carga y error. Cuando la app esté 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. Abre BindingAdapters.kt. Agrega un nuevo adaptador de vinculación llamado bindStatus() que toma un valor ImageView y un valor MarsApiStatus como argumentos. Importa com.example.android.marsrealestate.overview.MarsApiStatus cuando se solicite.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView, 
          status: MarsApiStatus?) {
}
  1. Agrega un elemento 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 Glide en la tarea anterior. Importa android.view.View cuando se solicite.
when (status) {
   MarsApiStatus.LOADING -> {
      statusImageView.visibility = View.VISIBLE
      statusImageView.setImageResource(R.drawable.loading_animation)
   }
}
  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 reutiliza 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 desactiva la visibilidad del estado ImageView para ocultarla.
MarsApiStatus.DONE -> {
   statusImageView.visibility = View.GONE
}

Paso 3: Agrega el estado ImageView al diseño

  1. Abre res/layout/fragment_overview.xml. Debajo del elemento RecyclerView, dentro de ConstraintLayout, agrega el ImageView que se muestra a continuación.

    Este ImageView tiene las mismas restricciones que el 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, que tiene la vista de llamada a tu BindingAdapter cuando cambia la propiedad de estado en el modelo de vista.
<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}" />
  1. Activa el modo de avión en el emulador o dispositivo para simular una conexión de red faltante. Compila y ejecuta la app, y observa la imagen de error que aparece:

  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.

Proyecto de Android Studio: MarsRealEstateGrid

  • Si quieres simplificar el proceso de administración de imágenes, usa la biblioteca Glide para descargar, almacenar en búfer, decodificar y almacenar en caché imágenes en tu app.
  • Glide necesita dos cosas para cargar una imagen de Internet: la URL de una imagen y un objeto ImageView para colocarla. Para especificar estas opciones, usa los métodos load() y into() con Glide.
  • 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 Glide a fin de cargar una imagen de una URL a una ImageView.
  • Los adaptadores de vinculación son métodos de extensión anotados con la anotación @BindingAdapter.
  • Para agregar opciones a la solicitud de Glide, usa el método apply(). Por ejemplo, usa apply() con placeholder() para especificar un elemento de diseño de carga y apply() con error() para especificar un elemento de diseño con error.
  • Para producir 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.

Curso de Udacity:

Documentación para desarrolladores de Android:

Otra:

En esta sección, se enumeran las posibles tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna la tarea.
  • Informa a los alumnos cómo enviar los deberes.
  • Califica las tareas.

Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.

Responda estas preguntas

Pregunta 1

¿Qué método de deslizamiento se usa para indicar el ImageView que contendrá la imagen cargada?

into()

with()

imageview()

apply()

Pregunta 2

¿Cómo especifico una imagen de marcador de posición para mostrar cuando se carga el deslizamiento?

▢ Usa el método into() con un elemento de diseño.

▢ Usa RequestOptions() y llama al método placeholder() con un elemento de diseño.

▢ Asigna la propiedad Glide.placeholder a un elemento de diseño.

▢ Usa RequestOptions() y llama al método loadingImage() con un elemento de diseño.

Pregunta 3

¿Cómo indicas que un método es un adaptador de vinculación?

▢ Llama al método setBindingAdapter() en el objeto LiveData.

▢ Coloca el método en un archivo Kotlin llamado BindingAdapters.kt.

▢ Usa el atributo android:adapter en el diseño XML.

▢ Anota el método con @BindingAdapter.

Comience la siguiente lección: 8.3 Filtros y vistas detalladas con datos de Internet

Para ver vínculos a otros codelabs de este curso, consulta la página de destino de codelabs sobre aspectos básicos de Kotlin para Android.