Introducción
En el codelab anterior, aprendiste cómo obtener datos de un servicio web y analizar la respuesta en un objeto Kotlin. En este codelab, aprenderás a cargar y mostrar fotos desde una URL web. También puedes revisar cómo compilar un objeto RecyclerView
y usarlo para mostrar una cuadrícula de imágenes en la página de descripción general.
Requisitos previos
- Cómo crear y usar fragmentos
- Cómo recuperar JSON de un servicio web de REST y analizar esos datos en objetos de Kotlin con las bibliotecas Retrofit y Moshi
- Cómo construir un diseño de cuadrícula con
RecyclerView
- Cómo funcionan
Adapter
,ViewHolder
yDiffUtil
Qué aprenderás
- Cómo usar la biblioteca Coil para cargar y mostrar una imagen desde una URL web
- Cómo usar
RecyclerView
y un adaptador de cuadrícula para mostrar una cuadrícula de imágenes - Cómo manejar los posibles errores mientras se descargan y se muestran las imágenes
Qué compilarás
- Modificarás la app de MarsPhotos para obtener la URL de la imagen de los datos de Marte y usarás Coil para cargar y mostrar esa imagen.
- Agregarás una animación de carga y un ícono de error a la app.
- Usarás
RecyclerView
para mostrar una cuadrícula de imágenes de Marte. - Agregarás administración de estado y errores a
RecyclerView
.
Requisitos
- Una computadora con un navegador web moderno, como la versión más reciente de Chrome
- Tener acceso a Internet en la computadora
En este codelab, continuarás trabajando con la app del codelab anterior, MarsPhotos. La app de MarsPhotos se conecta a un servicio web para recuperar y mostrar la cantidad de objetos de Kotlin recuperados con Retrofit. Estos objetos de Kotlin contienen las URL de las fotos reales de la superficie de Marte capturadas por los rovers de la NASA.
La versión de la app que compilarás en este codelab completará la página de descripción general, la cual muestra fotos de Marte en una cuadrícula de imágenes. Las imágenes son parte de los datos que tu app recuperó del servicio web de Marte. Tu app usará la biblioteca de Coil para cargar y mostrar las imágenes, y un RecyclerView
a fin de crear el diseño de cuadrícula para las imágenes. Además, la app manejará correctamente los errores de red.
Mostrar una foto de una URL web puede parecer sencillo, pero se necesita un poco de ingeniería para que funcione bien. La imagen se debe descargar, almacenar de forma interna y decodificar de su formato comprimido a una imagen que Android pueda usar. La imagen debe almacenarse en una memoria caché, en una caché basada en almacenamiento o ambas. Todo esto tiene que ocurrir en subprocesos en segundo plano de baja prioridad para que la IU siga siendo receptiva. Además, para obtener el mejor rendimiento de red y CPU, te recomendamos recuperar y decodificar más de una imagen a la vez.
Afortunadamente, puedes usar una biblioteca creada por la comunidad llamada Coil para descargar, almacenar en búfer, decodificar y almacenar en caché tus imágenes. Sin usar Coil, tendrías mucho más trabajo.
Básicamente, Coil necesita dos cosas:
- La URL de la imagen que quieres cargar y mostrar.
- Un objeto
ImageView
para mostrar esa imagen.
En esta tarea, aprenderás a utilizar Coil para mostrar una sola imagen del servicio web de Marte. Debes mostrar la imagen de la primera foto de Marte en la lista de fotos que muestra el servicio web. Estas son las capturas de pantalla de antes y después:
Cómo agregar una dependencia de Coil
- 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 bloquerepositories
superior.
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 llamadophotos
del 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
. CambialistResult
a_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.value
a 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
TextView
muestra la URL de la primera foto de Marte. Todo lo que hiciste hasta ahora es configurarViewModel
yLiveData
para 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 unImageView
y unString
como parámetros.
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
Importa android.widget.ImageView
cuando se solicite.
- Anota la función con
@BindingAdapter
. La anotación@BindingAdapter
indica a la vinculación de datos que ejecute este adaptador de vinculación cuando un elemento View tiene el 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 argumentoimageURL
mediante el operador de llamada segura.
imgUrl?.let {
}
- Dentro del bloque
let{}
, agrega la siguiente línea para convertir la string de URL en un objetoUri
con 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 objetoimgUri
aimgView
.
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:imageUrl
al elementoImageView
para usar el nuevo adaptador de vinculación de carga de imágenes. Recuerda que elphotos
contiene una listaMarsPhotos
recuperada 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 claseFragmentOverviewBinding
y la asigna a la variable de vinculación. Verás errores debido a la eliminación de esta línea. Son temporales; los corregirás más adelante.
//val binding = FragmentOverviewBinding.inflate(inflater)
- Usa
grid_view_item.xml
en lugar defragment_overview.xml.
Agrega la siguiente línea para aumentar la claseGridViewItemBinding
en 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.xml
y haz clic en la pestaña Design de la derecha. Para la imagen de error, utiliza el ícono de imagen rota que se encuentra disponible en la biblioteca de íconos integrada. Este elemento de diseño vectorial usa el atributoandroid:tint
para 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
_photos
para que sea una lista de objetosMarsPhoto
.
private val _photos = MutableLiveData<List<MarsPhoto>>()
- Reemplaza el tipo de propiedad
photos
de 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/catch
completo ahora tiene el siguiente aspecto:
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
GridLayout
El objeto GridLayoutManager
de RecyclerView
define los datos como una cuadrícula desplazable, como se muestra a continuación.
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 variablephoto
del tipoMarsPhoto
.
<data>
<variable
name="photo"
type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
- En
<ImageView>
, cambia el atributoapp:imageUrl
para 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
,width
yheight
para0dp
, de modo que ocupe elConstraintLayout
superior. Usarás un diseño de cuadrícula, por lo que debes establecer el atributolayoutManager
enandroidx.recyclerview.widget.GridLayoutManager
. Establece unspanCount
en2
para que haya dos columnas.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager=
"androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2" />
- Para obtener una vista previa de cómo se ve el código anterior en la vista Design, usa
tools:itemCount
a fin de configurar la cantidad de elementos que se muestran en nuestro diseño en16
. El atributoitemCount
especifica la cantidad de elementos que el editor de diseño debe procesar en la ventana Preview. Establece el diseño de los elementos de la lista engrid_view_item
contools: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_item
para cada elemento de cuadrícula derecyclerview
.
- De acuerdo con los lineamientos de Material Design, debes tener
8dp
de espacio en la parte superior, inferior y lateral de la lista, y4dp
de espacio entre los elementos. Puedes lograr esto con una combinación de relleno en el diseñofragment_overview.xml
y en el diseñogridview_item.xml
.
- Abre
layout/gridview_item.xml
. Observa el atributopadding
; ya tienes2dp
de relleno entre el exterior del elemento y el contenido. Eso nos dará4dp
del espacio entre el contenido del elemento y2dp
en los bordes externos, lo que significa que necesitaremos6dp
adicionales de relleno en los bordes externos para cumplir con los lineamientos de diseño. - Vuelve a
layout/fragment_overview.xml
. Agrega6dp
de relleno paraRecyclerView
, por lo que tendrás8dp
en el exterior y4dp
en 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
PhotoGridAdapter
deListAdapter
con los parámetros del constructor que se muestran a continuación. La clasePhotoGridAdapter
extiendeListAdapter
, 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 variableGridViewItemBinding
para vincularMarsPhoto
al diseño, así que pasa la variable aMarsPhotoViewHolder
. La clase baseViewHolder
requiere una vista en su constructor, le pasas la vista raíz de vinculación.
class MarsPhotoViewHolder(private var binding:
GridViewItemBinding):
RecyclerView.ViewHolder(binding.root) {
}
Importa androidx.recyclerview.widget.RecyclerView
y com.example.android.marsrealestate.databinding.GridViewItemBinding
si se te solicita.
- En
MarsPhotoViewHolder
, crea un métodobind()
que reciba un objetoMarsPhoto
como argumento y establezcabinding.property
en 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
PhotoGridAdapter
deonCreateViewHolder()
, quita el comentario TODO y agrega la línea que se muestra a continuación. El métodoonCreateViewHolder()
debe mostrar unMarsPhotoViewHolder
nuevo, creado mediante el aumento delGridViewItemBinding
y el uso deLayoutInflater
de tu contexto deViewGroup
principal.
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 objetoMarsPhoto
asociado con la posiciónRecyclerView
actual 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 objetoDiffCallback
extiendeDiffUtil.ItemCallback
con el tipo genérico de objeto que deseas comparar:MarsPhoto
. Vas a comparar dos objetos de fotos de Marte dentro de la implementación.
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
}
Importa androidx.recyclerview.widget.DiffUtil
cuando se solicite.
- 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
.DiffUtil
llama a este método para decidir si dos objetos representan el mismo elemento.DiffUtil
usa este método para determinar si el objetoMarsPhoto
nuevo es el mismo que el objetoMarsPhoto
anterior. El ID de cada elemento (objetoMarsPhoto
) es único. Compara los ID deoldItem
ynewItem
, y muestra el resultado.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
- En
areContentsTheSame()
, quita elTODO
.DiffUtil
llama a este método cuando desea verificar si dos elementos tienen los mismos datos. Los datos importantes de MarsPhoto son la URL de la imagen. Compara las URL deoldItem
ynewItem
, 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 unRecyclerView
y una lista de objetosMarsPhoto
como argumentos. Anota ese método con@BindingAdapter
con 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.adapter
aPhotoGridAdapter
y 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 aRecyclerView
cuá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
bindRecyclerView
completo debe verse de la siguiente manera:
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
- Para conectar todo, abre
res/layout/fragment_overview.xml
. Agrega el atributoapp:listData
al elementoRecyclerView
y establécelo enviewmodel.photos
con la vinculación de datos. Esto es similar a lo que hiciste paraImageView
en una tarea anterior.
app:listData="@{viewModel.photos}"
- Abre
overview/OverviewFragment.kt
. EnonCreateView()
, justo antes de la sentenciareturn
, inicializa el adaptadorRecyclerView
enbinding.photosGrid
en un objetoPhotoGridAdapter
nuevo.
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
RecyclerView
a 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
RecyclerView
que no recorte el contenido interno al relleno mediante el atributo android:clipToPadding. De este modo, se dibuja la vista de desplazamiento en el área de relleno. Vuelve alayout/fragment_overview.xml
. Agrega el atributoandroid:clipToPadding
para elRecyclerView
y 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 unenum
para representar todos los estados disponibles:
enum class MarsApiStatus { LOADING, ERROR, DONE }
- Desplázate hasta la definición de las propiedades
_status
ystatus
. Cambia los tipos deString
aMarsApiStatus. MarsApiStatus
como 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.DONE
y 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.LOADING
sobre 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_photos
en 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.kt
y desplázate hasta el final del archivo. Agrega un nuevo adaptador de vinculación llamadobindStatus()
que toma un valorImageView
y un valorMarsApiStatus
como argumentos. Anota el método con@BindingAdapter
y pasa el atributo personalizadomarsApiStatus
como 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 elImageView
como visible y asígnale la animación de carga. Este es el mismo elemento de diseño de animación que usaste para Coil en la tarea anterior.
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
}
Importa android.view.View
si se te solicita.
- Agrega un caso para el estado Error, que es
MarsApiStatus.ERROR
. De manera similar a lo que hiciste para el estadoLOADING
, configura el estadoImageView
como visible y usa el elemento de diseño de error de conexión.
MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
- Agrega un caso para el estado Done, que es
MarsApiStatus.DONE
. Aquí tienes una respuesta correcta, así que configura el nivel de visibilidad del estadoImageView
comoView.
GONE
para ocultarlo.
MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
Configuraste el adaptador de vinculación para la vista de imagen de estado. En el siguiente paso, agregarás una vista de imagen que use el nuevo adaptador de vinculación.
Cómo agregar el estado ImageView
En este paso, agregarás la vista de imagen en el fragment_overview.xml
que mostrará el estado que definiste antes.
- Abre
res/layout/fragment_overview.xml
. Dentro deConstraintLayout
, debajo del elementoRecyclerView
, agrega laImageView
que se muestra a continuación.
<ImageView
android:id="@+id/status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:marsApiStatus="@{viewModel.status}" />
La ImageView
anterior tiene las mismas restricciones que RecyclerView
. Sin embargo, el ancho y el alto usan wrap_content
para centrar la imagen en lugar de estirarla para llenar la vista. Observa también el atributo app:marsApiStatus
establecido en viewModel.status
, que llama a tu BindingAdapter
cuando la propiedad de estado de ViewModel
cambia.
- 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
RecyclerView
con unGridLayoutManager
. - Para actualizar la lista de propiedades cuando cambie, usa un adaptador de vinculación entre
RecyclerView
y el diseño.
Documentación para desarrolladores de Android:
- Descripción general de ViewModel
- Descripción general de LiveData
- Corrutinas, documentación oficial
- Adaptadores de vinculación
Otra: