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,ViewHolderyDiffUtil
Qué aprenderás
- Cómo usar la biblioteca Coil para cargar y mostrar una imagen desde una URL web
- Cómo usar
RecyclerViewy 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
RecyclerViewpara 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.

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
ImageViewpara 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
- Abre la app de MarsPhotos solution del codelab anterior.
- Ejecuta la app para ver qué hace. (Muestra la cantidad total de fotos de Marte obtenidas).
- Abre build.gradle (Module: app).
- 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.
- La biblioteca de Coil está alojada y disponible en el repositorio
mavenCentral(). En build.gradle (Project: MarsPhotos), agregamavenCentral()al bloquerepositoriessuperior.
repositories {
google()
jcenter()
mavenCentral()
}
- 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.
- Abre
overview/OverviewViewModel.kt. Debajo de la declaración de propiedad_status, agrega una nueva propiedad mutable llamada_photos, del tipoMutableLiveData, que pueda almacenar un solo objetoMarsPhoto.
private val _photos = MutableLiveData<MarsPhoto>()
Importa com.example.android.marsphotos.network.MarsPhoto cuando se solicite.
- Debajo de la declaración
_photos, agrega un campo de copia de seguridad público llamadophotosdel tipoLiveData<MarsPhoto>.
val photos: LiveData<MarsPhoto> = _photos
- En el método
getMarsPhotos(), dentro del bloquetry{}, busca la línea que establece los datos recuperados del servicio web comolistResult..
try {
val listResult = MarsApi.retrofitService.getPhotos()
...
}
- Asigna la primera foto de Marte recuperada a la nueva variable
_photos. CambialistResulta_photos.value. Asigna la primera URL de las fotos al índice0. Se mostrará un error; lo corregirás más tarde.
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
...
}
- En la siguiente línea, actualiza
status.valuea lo siguiente. Usa los datos de la propiedad nueva en lugar delistResult. Muestra la URL de la primera imagen de la lista de fotos.
try {
...
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- 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}"
}
- Ejecuta la app. Ahora
TextViewmuestra la URL de la primera foto de Marte. Todo lo que hiciste hasta ahora es configurarViewModelyLiveDatapara esa URL.

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
- En el paquete
com.example.android.marsphotos, crea un archivo de Kotlin llamadoBindingAdapters. Este archivo contendrá los adaptadores de vinculación que usas en toda la app.

- In
BindingAdapters.kt, crea una funciónbindImage()que tome unImageViewy unStringcomo parámetros.
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
Importa android.widget.ImageView cuando se solicite.
- Anota la función con
@BindingAdapter. La anotación@BindingAdapterindica a la vinculación de datos que ejecute este adaptador de vinculación cuando un elemento View tiene el atributoimageUrl.
@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.
- Dentro de la función
bindImage(), agrega un bloquelet{}al argumentoimageURLmediante el operador de llamada segura.
imgUrl?.let {
}
- Dentro del bloque
let{}, agrega la siguiente línea para convertir la string de URL en un objetoUricon el métodotoUri(). Para usar el esquema HTTPS, agregabuildUpon.scheme("https")al compilador detoUri. Llama abuild()para compilar el objeto.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Importa androidx.core.net.toUri cuando se solicite.
- Dentro del bloque
let{}, después de la declaraciónimgUri, usaload(){}de Coil para cargar la imagen del objetoimgUriaimgView.
imgView.load(imgUri) {
}
Importa coil.load cuando se solicite.
- 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.
- Abre
res/layout/grid_view_item.xml. - Por encima del elemento
<ImageView>, agrega un elemento<data>para la vinculación de datos y realiza la vinculación a la claseOverviewViewModel:
<data>
<variable
name="viewModel"
type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
- Agrega un atributo
app:imageUrlal elementoImageViewpara usar el nuevo adaptador de vinculación de carga de imágenes. Recuerda que elphotoscontiene una listaMarsPhotosrecuperada del servidor. Asigna la primera URL de entrada al atributoimageUrl.
<ImageView
android:id="@+id/mars_image"
...
app:imageUrl="@{viewModel.photos.imgSrcUrl}"
... />
- Abre
overview/OverviewFragment.kt. En el métodoonCreateView(), comenta la línea que aumenta la claseFragmentOverviewBindingy 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)
- Usa
grid_view_item.xmlen lugar defragment_overview.xml.Agrega la siguiente línea para aumentar la claseGridViewItemBindingen su lugar.
val binding = GridViewItemBinding.inflate(inflater)
Importa com.example.android.marsphotos. databinding.GridViewItemBinding si se te solicita.
- Ejecuta la app. Ahora deberías ver una sola imagen de Marte.

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.
- Abre
res/drawable/ic_broken_image.xmly 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 atributoandroid:tintpara colorear el ícono gris.

- 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).

- Regresa al archivo
BindingAdapters.kt. En el métodobindImage(), actualiza la llamada aimgView.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ñoloading_animation). Este código también configura una imagen para usarla si falla la carga (elemento de diseñobroken_image).
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_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()
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
}
}
- 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.

- Revierte los cambios temporales que realizaste en
overview/OverviewFragment.kt. En el métodoonCreateview(), quita el comentario de la línea que aumentaFragmentOverviewBinding. Borra o comenta la línea que aumentaGridViewIteMBinding.
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.
- Abre
overview/OverviewViewModel.kt. - Cambia el tipo
_photospara que sea una lista de objetosMarsPhoto.
private val _photos = MutableLiveData<List<MarsPhoto>>()
- Reemplaza el tipo de propiedad
photosde copia de seguridad al tipoList<MarsPhoto>también:
val photos: LiveData<List<MarsPhoto>> = _photos
- Desplázate hacia abajo hasta el bloque
try {}dentro del métodogetMarsPhotos().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"
- El bloque
try/catchcompleto 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.

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.
- Abre
layout/gridview_item.xml. Quita la variable de datosviewModel. - Dentro de la etiqueta
<data>, agrega la siguiente variablephotodel tipoMarsPhoto.
<data>
<variable
name="photo"
type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
- En
<ImageView>, cambia el atributoapp:imageUrlpara hacer referencia a la URL de la imagen en el objetoMarsPhoto. Estos cambios revierten los cambios temporales que realizaste en la tarea anterior.
app:imageUrl="@{photo.imgSrcUrl}"
- Abre
layout/fragment_overview.xml. Borra todo el elemento<TextView> - En su lugar, agrega el siguiente elemento
<RecyclerView>. Configura los atributosphotos_grid,widthyheightpara0dp, de modo que ocupe elConstraintLayoutsuperior. Usarás un diseño de cuadrícula, por lo que debes establecer el atributolayoutManagerenandroidx.recyclerview.widget.GridLayoutManager. Establece unspanCounten2para 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" />
- Para obtener una vista previa de cómo se ve el código anterior en la vista Design, usa
tools:itemCounta fin de configurar la cantidad de elementos que se muestran en nuestro diseño en16. El atributoitemCountespecifica 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 engrid_view_itemcontools:listitem.
<androidx.recyclerview.widget.RecyclerView
...
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
- 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_itempara cada elemento de cuadrícula derecyclerview.

- De acuerdo con los lineamientos de Material Design, debes tener
8dpde espacio en la parte superior, inferior y lateral de la lista, y4dpde espacio entre los elementos. Puedes lograr esto con una combinación de relleno en el diseñofragment_overview.xmly en el diseñogridview_item.xml.

- Abre
layout/gridview_item.xml. Observa el atributopadding; ya tienes2dpde relleno entre el exterior del elemento y el contenido. Eso nos dará4dpdel espacio entre el contenido del elemento y2dpen los bordes externos, lo que significa que necesitaremos6dpadicionales de relleno en los bordes externos para cumplir con los lineamientos de diseño. - Vuelve a
layout/fragment_overview.xml. Agrega6dpde relleno paraRecyclerView, por lo que tendrás8dpen el exterior y4dpen el interior, como en los lineamientos.
<androidx.recyclerview.widget.RecyclerView
...
android:padding="6dp"
... />
- 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.
- En el paquete
overview, crea una nueva clase de Kotlin llamadaPhotoGridAdapter.kt. - Extiende la clase
PhotoGridAdapterdeListAdaptercon los parámetros del constructor que se muestran a continuación. La clasePhotoGridAdapterextiendeListAdapter, cuyo constructor necesita el tipo de elemento de lista, el contenedor de vistas y una implementaciónDiffUtil.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.
- 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étodosListAdapter,onCreateViewHolder()yonBindViewHolder(). 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.
- Dentro del
PhotoGridAdapter, agrega una definición de clase interna paraMarsPhotoViewHolder, que extiendeRecyclerView.ViewHolder. Necesitas la variableGridViewItemBindingpara vincularMarsPhotoal diseño, así que pasa la variable aMarsPhotoViewHolder. La clase baseViewHolderrequiere 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.
- En
MarsPhotoViewHolder, crea un métodobind()que reciba un objetoMarsPhotocomo argumento y establezcabinding.propertyen ese objeto. Llama aexecutePendingBindings()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()
}
- Dentro de la clase
PhotoGridAdapterdeonCreateViewHolder(), quita el comentario TODO y agrega la línea que se muestra a continuación. El métodoonCreateViewHolder()debe mostrar unMarsPhotoViewHoldernuevo, creado mediante el aumento delGridViewItemBindingy el uso deLayoutInflaterde tu contexto deViewGroupprincipal.
return MarsPhotoViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
Importa android.view.LayoutInflater si se te solicita.
- En el método
onBindViewHolder(), quita el comentario TODO y agrega las líneas que se muestran a continuación. Aquí llamas agetItem()para obtener el objetoMarsPhotoasociado con la posiciónRecyclerViewactual y, luego, pasas esa propiedad al métodobind()enMarsPhotoViewHolder.
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
- Dentro de
PhotoGridAdapter, agrega una definición de objeto complementario paraDiffCallback, como se muestra a continuación.
El objetoDiffCallbackextiendeDiffUtil.ItemCallbackcon 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.
- Presiona la bombilla roja para implementar los métodos del comparador del objeto
DiffCallback, que sonareItemsTheSame()yareContentsTheSame().
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented")
}
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented") }
- En el método
areItemsTheSame(), quita elTODO.DiffUtilllama a este método para decidir si dos objetos representan el mismo elemento.DiffUtilusa este método para determinar si el objetoMarsPhotonuevo es el mismo que el objetoMarsPhotoanterior. El ID de cada elemento (objetoMarsPhoto) es único. Compara los ID deoldItemynewItem, y muestra el resultado.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
- En
areContentsTheSame(), quita elTODO.DiffUtilllama 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 deoldItemynewItem, 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.
- Abre
BindingAdapters.kt. - Al final del archivo, agrega un método
bindRecyclerView()que tome unRecyclerViewy una lista de objetosMarsPhotocomo argumentos. Anota ese método con@BindingAdaptercon el atributolistData.
@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.
- Dentro de la función
bindRecyclerView(), transmiterecyclerView.adapteraPhotoGridAdaptery asígnalo a un nuevoadapter.de la propiedadval.
val adapter = recyclerView.adapter as PhotoGridAdapter
- Al final de la función
bindRecyclerView(), llama aadapter.submitList()con los datos de la lista de fotos de Marte. Esto le indica aRecyclerViewcuándo hay una lista nueva disponible.
adapter.submitList(data)
Importa com.example.android.marsrealestate.overview.PhotoGridAdapter si se te solicita.
- El adaptador de vinculación
bindRecyclerViewcompleto debe verse de la siguiente manera:
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
- Para conectar todo, abre
res/layout/fragment_overview.xml. Agrega el atributoapp:listDataal elementoRecyclerViewy establécelo enviewmodel.photoscon la vinculación de datos. Esto es similar a lo que hiciste paraImageViewen una tarea anterior.
app:listData="@{viewModel.photos}"
- Abre
overview/OverviewFragment.kt. EnonCreateView(), justo antes de la sentenciareturn, inicializa el adaptadorRecyclerViewenbinding.photosGriden un objetoPhotoGridAdapternuevo.
binding.photosGrid.adapter = PhotoGridAdapter()
- 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
RecyclerViewa medida que te desplazas, por lo que nunca parece que te desplazas por la lista en la barra de acciones.

- Para solucionar este problema, debes indicar a
RecyclerViewque 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 alayout/fragment_overview.xml. Agrega el atributoandroid:clipToPaddingpara elRecyclerViewy configúralo comofalse.
<androidx.recyclerview.widget.RecyclerView
...
android:clipToPadding="false"
... />
- 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.

- 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.

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.
- Activa el modo de avión en el dispositivo o el emulador. Ejecuta la app desde Android Studio. Observa la pantalla en blanco.

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.
- Abre
overview/OverviewViewModel.kt. En la parte superior del archivo (después de las importaciones, antes de la definición de clase), agrega unenumpara representar todos los estados disponibles:
enum class MarsApiStatus { LOADING, ERROR, DONE }
- Desplázate hasta la definición de las propiedades
_statusystatus. Cambia los tipos deStringaMarsApiStatus. MarsApiStatuscomo la clase de enum que definiste en el paso anterior.
private val _status = MutableLiveData<MarsApiStatus>()
val status: LiveData<MarsApiStatus> = _status
- En el método
getMarsPhotos(), cambia la string"Success: ..."al estadoMarsApiStatus.DONEy la string"Failure..."aMarsApiStatus.ERROR.
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception)
_status.value = MarsApiStatus.ERROR
}
- Establece el estado en
MarsApiStatus.LOADINGsobre el bloquetry {}. Es el estado inicial mientras se ejecuta la corrutina y esperas los datos. El bloqueviewModelScope.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
}
}
- Después del estado de error en el bloque
catch {}, establece_photosen una lista vacía. Esta acción borrará la vista de reciclador.
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
- 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.
- Para agregar otro adaptador, abre
BindingAdapters.kty desplázate hasta el final del archivo. Agrega un nuevo adaptador de vinculación llamadobindStatus()que toma un valorImageViewy un valorMarsApiStatuscomo argumentos. Anota el método con@BindingAdaptery pasa el atributo personalizadomarsApiStatuscomo parámetro.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: MarsApiStatus?) {
}
Importa com.example.android.marsrealestate.overview.MarsApiStatus si se te solicita.
- Agrega un bloque
when {}dentro del métodobindStatus()para alternar entre los diferentes estados.
when (status) {
}
- Dentro del
when {}, agrega un caso para el estado de carga (MarsApiStatus.LOADING). Para este estado, configura elImageViewcomo 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.
- Agrega un caso para el estado Error, que es
MarsApiStatus.ERROR. De manera similar a lo que hiciste para el estadoLOADING, configura el estadoImageViewcomo 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)
}
- Agrega un caso para el estado Done, que es
MarsApiStatus.DONE. Aquí tienes una respuesta correcta, así que configura el nivel de visibilidad del estadoImageViewcomoView.GONEpara 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.
- Abre
res/layout/fragment_overview.xml. Dentro deConstraintLayout, debajo del elementoRecyclerView, agrega laImageViewque 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.
- 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:

- 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
- Haz clic en la URL proporcionada. Se abrirá la página de GitHub del proyecto en un navegador.
- En esa página, haz clic en el botón Code, que abre un cuadro de diálogo.
- 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.
- Ubica el archivo en tu computadora (probablemente en la carpeta Descargas).
- 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
- Inicia Android Studio.
- En la ventana Welcome to Android Studio, haz clic en Open an existing Android Studio project.
Nota: Si Android Studio ya está abierto, selecciona la opción de menú File > New > Import Project.
- En el cuadro de diálogo Import Project, navega hasta donde se encuentra la carpeta de proyecto descomprimido (probablemente en Descargas).
- Haz doble clic en la carpeta del proyecto.
- Espera a que Android Studio abra el proyecto.
- Haz clic en el botón Run
para compilar y ejecutar la app. Asegúrate de que funcione como se espera.
- 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
RecyclerViewcon unGridLayoutManager. - Para actualizar la lista de propiedades cuando cambie, usa un adaptador de vinculación entre
RecyclerViewy el diseño.
Documentación para desarrolladores de Android:
- Descripción general de ViewModel
- Descripción general de LiveData
- Corrutinas, documentación oficial
- Adaptadores de vinculación
Otra:



