Aspectos básicos de Kotlin para Android 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 de forma secuencial. Todos los codelabs del curso se enumeran en la página de destino de los codelabs de Android Kotlin Fundamentals.

Introducción

En el codelab anterior, aprendiste a obtener datos de un servicio web y a 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 los modelos de vistas, los factores de modelos de vistas, las 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

  • Modificarás la app de MarsRealEstate para obtener la URL de la imagen de los datos de la propiedad de Marte y usarás Glide 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 propiedades de Marte.
  • Agregarás administración de estado y errores a RecyclerView.

En este codelab (y en los 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 propiedades, incluidos detalles como el precio y si la propiedad está disponible para la venta o el alquiler. Las imágenes que representan cada propiedad son fotografías reales de Marte capturadas por los rovers marcianos de la NASA.

La versión de la app que compiles 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 la propiedad que tu app obtiene del servicio web de bienes raíces de Marte. Tu app usará la biblioteca de 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 se debe descargar, almacenar en búfer 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. Aprender a cargar imágenes de la red de manera eficaz 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 ahorra mucho trabajo en comparación con si tuvieras que hacer todo esto desde cero.

Básicamente, Glide necesita dos cosas:

  • Es la URL de la imagen que quieres 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 los detalles de texto de una propiedad que, hipotéticamente, está 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 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, actualiza la clase OverviewViewModel para incluir datos en vivo de una sola propiedad de Marte.

  1. Abre overview/OverviewViewModel.kt. Justo debajo del LiveData para el _response, agrega datos dinámicos internos (mutables) y externos (inmutables) para un solo objeto MarsProperty.

    Importa la clase MarsProperty (com.example.android.marsrealestate.network.MarsProperty) cuando se te 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 hay objetos MarsProperty disponibles, esta prueba establece el valor de _property LiveData en la primera propiedad de 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 vincularlo al componente imgSrcUrl del property LiveData:
android:text="@{viewModel.property.imgSrcUrl}"
  1. Ejecuta la app. El elemento 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 momento 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 Glide para cargar la imagen. Los adaptadores de vinculación son métodos de extensión que se encuentran entre una vista y los datos vinculados para proporcionar un comportamiento personalizado cuando cambian los datos. En este caso, el comportamiento personalizado consiste en llamar a Glide para cargar una imagen desde una URL en un 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 se ejecute este adaptador de vinculación 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 cadena de URL (del XML) en un objeto Uri. Importa androidx.core.net.toUri cuando se te 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 que forma parte de la clase String.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
  1. Aún dentro de let {}, llama a Glide.with() para cargar la imagen del objeto Uri en el ImageView. Importa com.bumptech.glide.Glide cuando se te solicite.
Glide.with(imgView.context)
       .load(imgUri)
       .into(imgView)

Paso 4: Actualiza el diseño y los fragmentos

Aunque Glide cargó la imagen, aún no hay nada que 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. Aquí lo usas de forma temporal 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 él más adelante.
//val binding = FragmentOverviewBinding.inflate(inflater)
  1. Agrega una línea para aumentar la clase GridViewItemBinding en su lugar. Importa com.example.android.marsrealestate. databinding.GridViewItemBinding cuando se solicite.
val binding = GridViewItemBinding.inflate(inflater)
  1. Ejecuta la app. Ahora deberías ver la foto de la imagen del primer MarsProperty en la lista de resultados.

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

Glide puede mejorar la experiencia del usuario mostrando 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 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. (no ves la animación 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 te solicite.

    Este código establece 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; lo solucionarás en la última parte del codelab.

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

Paso 1: Actualiza el modelo de vista

En este momento, el modelo de vistas tiene un _property LiveData que contiene un objeto MarsProperty, el primero en 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 en tiempo real externos property por properties. Agrega la lista al tipo LiveData aquí también:
 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. Como la variable listResult contiene una lista de objetos MarsProperty, puedes asignarla a _properties.value en lugar de probar si la respuesta fue exitosa.
_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 los 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 tarea anterior.
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 GridLayoutManager y el diseño 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 fragment_overview tiene un RecyclerView, mientras que el diseño grid_view_item tiene un solo ImageView. En este paso, vincularás 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 te 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 lugar 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 para implementar los métodos del comparador de 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. En el método areItemsTheSame(), quita el comentario TODO. Usa el operador de igualdad referencial de Kotlin (===), que devuelve true si las referencias de objeto para oldItem y newItem son las mismas.
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 te solicite.

    Necesitas la variable GridViewItemBinding para vincular MarsProperty al diseño, así que pasa la variable a MarsPropertyViewHolder. Debido a que 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 devolver un MarsPropertyViewHolder nuevo, creado inflando el GridViewItemBinding y usando el LayoutInflater de tu contexto de ViewGroup principal.
   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 RecyclerView actual 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 elemento BindingAdapter para inicializar 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 imágenes de MarsProperty. 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 avión, las imágenes que aún no se cargaron se mostrará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 buena experiencia del usuario. 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. Mientras la app recupera la lista MarsProperty, mostrará la animación de carga.

Paso 1: Agrega estado al ViewModel

Para comenzar, crea un LiveData en el modelo de vista 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 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 activos _response internas y externas en toda la clase OverviewViewModel a _status. Como agregaste compatibilidad con _properties LiveData anteriormente en este codelab, la respuesta completa del servicio web no se usó. Necesitas un LiveData aquí para hacer un seguimiento del estado actual, por lo que puedes 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 aquí también. Cambia la cadena "Success" al estado MarsApiStatus.DONE y la cadena "Failure" a MarsApiStatus.ERROR.
  2. Agrega un estado MarsApiStatus.LOADING en 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 el _properties LiveData en una lista vacía. Esta acción borra 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 haces para que aparezca en la app? En este paso, usarás un ImageView, conectado a la vinculación de datos, para 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. 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 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 ocultarlo.
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 la ImageView que se muestra a continuación.

    Esta ImageView tiene las mismas restricciones que la 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 hace que la vista llame 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

  • Para simplificar el proceso de administración de imágenes, usa la biblioteca de Glide para descargar, almacenar en búfer, decodificar y almacenar en caché imágenes en tu app.
  • Glide necesita dos elementos para cargar una imagen de Internet: la URL de una imagen y un objeto ImageView en el que colocar la imagen. 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 desde una URL en un 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 cargando y usa apply() con error() para especificar un elemento de diseño de 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 actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna una tarea.
  • Comunicarles a los alumnos cómo enviar las actividades para el hogar.
  • Califica las actividades para el hogar.

Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.

Responde estas preguntas:

Pregunta 1

¿Qué método aplicable se usa para indicar el elemento 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?

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

▢ Usando RequestOptions() y llamando al método placeholder() con un elemento de diseño

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

▢ Usando RequestOptions() y llamando 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 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.

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

Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Conceptos básicos de Kotlin para Android.