Aspectos básicos de Kotlin para Android 07.4: Cómo interactuar con elementos RecyclerView

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

La mayoría de las apps que usan listas y cuadrículas que muestran elementos permiten que los usuarios interactúen con ellos. Presionar un elemento de una lista y ver sus detalles es un caso de uso muy común para este tipo de interacción. Para lograrlo, puedes agregar objetos de escucha de clics que respondan a las pulsaciones del usuario en los elementos mostrando una vista de detalles.

En este codelab, agregarás interacción a tu RecyclerView, basándote en una versión extendida de la app de Sleep Tracker de la serie anterior de codelabs.

Conocimientos que ya deberías tener

  • Compilar una interfaz de usuario básica con una actividad, fragmentos y vistas
  • Navegar entre fragmentos y usar safeArgs para pasar datos entre fragmentos
  • Ver modelos, fábricas de modelos, transformaciones y LiveData, y sus observadores
  • Cómo crear una base de datos Room, crear un objeto de acceso a datos (DAO) y definir entidades
  • Cómo usar corrutinas para bases de datos y otras tareas de larga duración
  • Cómo implementar un RecyclerView básico con un Adapter, un ViewHolder y un diseño de elemento
  • Cómo implementar la vinculación de datos para RecyclerView
  • Cómo crear y usar adaptadores de vinculación para transformar datos
  • Cómo usar GridLayoutManager

Qué aprenderás

  • Cómo hacer que los elementos de RecyclerView admitan clics Implementa un objeto de escucha de clics para navegar a una vista de detalles cuando se haga clic en un elemento.

Actividades

  • Compilarás una versión extendida de la app de TrackMySleepQuality del codelab anterior de esta serie.
  • Agrega un objeto de escucha de clics a tu lista y comienza a escuchar la interacción del usuario. Cuando se presiona un elemento de la lista, se activa la navegación a un fragmento con detalles sobre el elemento en el que se hizo clic. El código de partida proporciona código para el fragmento de detalles, así como el código de navegación.

La app de monitoreo del sueño inicial tiene dos pantallas, representadas por fragmentos, como se muestra en la siguiente figura.

La primera pantalla, que se muestra a la izquierda, tiene botones para iniciar y detener el seguimiento. En la pantalla, se muestran algunos de los datos de sueño del usuario. El botón Borrar borra de forma permanente todos los datos que la app recopiló del usuario. La segunda pantalla, que se muestra a la derecha, es para seleccionar una calificación de la calidad del sueño.

Esta app usa una arquitectura simplificada con un controlador de IU, un ViewModel y LiveData, y una base de datos Room para conservar los datos de sueño.

En este codelab, agregarás la capacidad de responder cuando un usuario presione un elemento de la cuadrícula, lo que mostrará una pantalla de detalles como la que se muestra a continuación. El código de esta pantalla (fragmento, modelo de vista y navegación) se proporciona con la app de inicio, y tú implementarás el mecanismo de control de clics.

Paso 1: Obtén la app inicial

  1. Descarga el código de inicio de RecyclerViewClickHandler desde GitHub y abre el proyecto en Android Studio.
  2. Compila y ejecuta la app de inicio del monitor de sueño.

[Opcional] Actualiza tu app si quieres usar la del codelab anterior

Si vas a trabajar con la app de partida proporcionada en GitHub para este codelab, ve al siguiente paso.

Si deseas seguir usando tu propia app de monitoreo del sueño que creaste en el codelab anterior, sigue las instrucciones que se indican a continuación para actualizar tu app existente de modo que tenga el código del fragmento de la pantalla de detalles.

  1. Incluso si continúas con tu app existente, obtén el código de inicio de RecyclerViewClickHandler de GitHub para que puedas copiar los archivos.
  2. Copia todos los archivos del paquete sleepdetail.
  3. En la carpeta layout, copia el archivo fragment_sleep_detail.xml.
  4. Copia el contenido actualizado de navigation.xml, que agrega la navegación para sleep_detail_fragment.
  5. En el paquete database, en SleepDatabaseDao, agrega el nuevo método getNightWithId():
/**
 * Selects and returns the night with given nightId.
*/
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
fun getNightWithId(key: Long): LiveData<SleepNight>
  1. En res/values/strings, agrega el siguiente recurso de cadena:
<string name="close">Close</string>
  1. Limpia y vuelve a compilar tu app para actualizar la vinculación de datos.

Paso 2: Inspecciona el código de la pantalla de detalles del sueño

En este codelab, implementarás un controlador de clics que navega a un fragmento que muestra detalles sobre la noche de sueño en la que se hizo clic. El código de partida ya contiene el fragmento y el gráfico de navegación para este SleepDetailFragment, ya que se trata de una gran cantidad de código, y los fragmentos y la navegación no forman parte de este codelab. Familiarízate con el siguiente código:

  1. En tu app, busca el paquete sleepdetail. Este paquete contiene el fragmento, el ViewModel y el ViewModelFactory para un fragmento que muestra los detalles de una noche de sueño.

  2. En el paquete sleepdetail, abre y revisa el código de SleepDetailViewModel. Este modelo de vista toma la clave para un SleepNight y un DAO en el constructor.

    El cuerpo de la clase tiene código para obtener el SleepNight para la clave determinada y la variable navigateToSleepTracker para controlar la navegación de vuelta a SleepTrackerFragment cuando se presiona el botón Cerrar.

    La función getNightWithId() devuelve un LiveData<SleepNight> y se define en SleepDatabaseDao (en el paquete database).

  3. En el paquete sleepdetail, abre y revisa el código de SleepDetailFragment. Observa la configuración de la vinculación de datos, el modelo de vista y el observador para la navegación.

  4. En el paquete sleepdetail, abre e inspecciona el código de SleepDetailViewModelFactory.

  5. En la carpeta de diseño, inspecciona fragment_sleep_detail.xml. Observa la variable sleepDetailViewModel definida en la etiqueta <data> para obtener los datos que se mostrarán en cada vista del modelo de vista.

    El diseño contiene un ConstraintLayout que incluye un ImageView para la calidad del sueño, un TextView para la calificación de calidad, un TextView para la duración del sueño y un Button para cerrar el fragmento de detalles.

  6. Abre el archivo navigation.xml. En el sleep_tracker_fragment, observa la nueva acción para el sleep_detail_fragment.

    La nueva acción, action_sleep_tracker_fragment_to_sleepDetailFragment, es la navegación desde el fragmento del monitor de sueño a la pantalla de detalles.

En esta tarea, actualizarás RecyclerView para que responda a las acciones del usuario mostrando una pantalla de detalles del elemento presionado.

Recibir clics y controlarlos es una tarea de dos partes: primero, debes escuchar y recibir el clic, y determinar en qué elemento se hizo clic. Luego, debes responder al clic con una acción.

Entonces, ¿cuál es el mejor lugar para agregar un objeto de escucha de clics para esta app?

  • El SleepTrackerFragment aloja muchas vistas, por lo que escuchar los eventos de clic a nivel del fragmento no te indicará qué elemento se hizo clic. Ni siquiera te dirá si se hizo clic en un elemento o en uno de los otros elementos de la IU.
  • Si se escucha a nivel de RecyclerView, es difícil determinar con exactitud en qué elemento de la lista hizo clic el usuario.
  • El mejor ritmo para obtener información sobre un elemento en el que se hizo clic se encuentra en el objeto ViewHolder, ya que representa un elemento de la lista.

Si bien ViewHolder es un buen lugar para detectar clics, no suele ser el mejor lugar para procesarlos. Entonces, ¿cuál es el mejor lugar para controlar los clics?

  • El objeto Adapter muestra elementos de datos en vistas para que puedas procesar clics en el adaptador. Sin embargo, la función de ese objeto es adaptar los datos para su visualización; no se encarga de la lógica de la app.
  • Por lo general, deberías procesar los clics en el ViewModel, ya que este tiene acceso a los datos y la lógica para determinar lo que debe suceder en respuesta al clic.ViewModel

Paso 1: Crea un objeto de escucha de clics y actívalo desde el diseño del elemento

  1. En la carpeta sleeptracker, abre SleepNightAdapter.kt.
  2. Al final del archivo, en el nivel superior, crea una nueva clase de escucha, SleepNightListener.
class SleepNightListener() {
    
}
  1. Dentro de la clase SleepNightListener, agrega una función onClick(). Cuando se hace clic en la vista que muestra un elemento de la lista, la vista llama a esta función onClick(). (Más adelante, establecerás la propiedad android:onClick de la vista en esta función).
class SleepNightListener() {
    fun onClick() = 
}
  1. Agrega un argumento de función night de tipo SleepNight a onClick(). La vista sabe qué elemento muestra, y esa información debe pasarse para controlar el clic.
class SleepNightListener() {
    fun onClick(night: SleepNight) = 
}
  1. Para definir lo que hace onClick(), proporciona una devolución de llamada clickListener en el constructor de SleepNightListener y asígnala a onClick().

    Asignarle un nombre a la expresión lambda que controla el clic, clickListener , ayuda a hacer un seguimiento de ella a medida que se pasa entre clases. La devolución de llamada clickListener solo necesita el night.nightId para acceder a los datos de la base de datos. La clase SleepNightListener terminada debería tener el siguiente aspecto:
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  1. Abre list_item_sleep_night.xml.
  2. Dentro del bloque data, agrega una variable nueva para que la clase SleepNightListener esté disponible a través de la vinculación de datos. Asigna al nuevo <variable> un name de clickListener.. Configura type como el nombre completamente calificado de la clase com.example.android.trackmysleepquality.sleeptracker.SleepNightListener, como se muestra a continuación. Ahora puedes acceder a la función onClick() en SleepNightListener desde este diseño.
<variable
            name="clickListener"
            type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />
  1. Para detectar clics en cualquier parte de este elemento de lista, agrega el atributo android:onClick al ConstraintLayout.

    Establece el atributo en clickListener:onClick(sleep) con una expresión lambda de vinculación de datos, como se muestra a continuación:
android:onClick="@{() -> clickListener.onClick(sleep)}"

Paso 2: Pasa el objeto de escucha de clics al objeto de vinculación y al titular de la vista

  1. Abre SleepNightAdapter.kt.
  2. Modifica el constructor de la clase SleepNightAdapter para que reciba un val clickListener: SleepNightListener. Cuando el adaptador vincule el ViewHolder, deberá proporcionarle este objeto de escucha de clics.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. En onBindViewHolder(), actualiza la llamada a holder.bind() para que también pase el objeto de escucha de clics al ViewHolder. Obtendrás un error del compilador porque agregaste un parámetro a la llamada a la función.
holder.bind(getItem(position)!!, clickListener)
  1. Agrega el parámetro clickListener a bind(). Para ello, coloca el cursor sobre el error y presiona Alt+Enter (Windows) o Option+Enter (Mac) sobre el error, como se muestra en la siguiente captura de pantalla.

  1. Dentro de la clase ViewHolder, dentro de la función bind(), asigna el objeto de escucha de clics al objeto binding. Verás un error porque debes actualizar el objeto de vinculación.
binding.clickListener = clickListener
  1. Para actualizar la vinculación de datos, Limpia y vuelve a compilar tu proyecto. (Es posible que también debas invalidar las memorias caché). Por lo tanto, tomaste un objeto de escucha de clics del constructor del adaptador y lo pasaste hasta el contenedor de vistas y el objeto de vinculación.

Paso 3: Muestra un mensaje emergente cuando se presiona un elemento

Ahora tienes el código para capturar un clic, pero no implementaste lo que sucede cuando se presiona un elemento de la lista. La respuesta más simple es mostrar un mensaje emergente que muestre el nightId cuando se hace clic en un elemento. Esto verifica que, cuando se hace clic en un elemento de la lista, se capture y se pase el nightId correcto.

  1. Abre SleepTrackerFragment.kt.
  2. En onCreateView(), busca la variable adapter. Observa que muestra un error, ya que ahora espera un parámetro de objeto de escucha de clics.
  3. Define un objeto de escucha de clics pasando una lambda al SleepNightAdapter. Esta lambda simple solo muestra un aviso que muestra el nightId, como se muestra a continuación. Deberás importar Toast. A continuación, se muestra la definición actualizada completa.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Ejecuta la app, presiona elementos y verifica que muestren un mensaje emergente con el nightId correcto. Dado que los elementos tienen valores de nightId crecientes y la app muestra la noche más reciente primero, el elemento con el valor de nightId más bajo se encuentra en la parte inferior de la lista.

En esta tarea, cambiarás el comportamiento cuando se hace clic en un elemento de RecyclerView, de modo que, en lugar de mostrar un mensaje Toast, la app navegará a un fragmento de detalles que muestra más información sobre la noche en la que se hizo clic.

Paso 1: Navegación con un clic

En este paso, en lugar de solo mostrar una notificación de advertencia, cambiarás la expresión lambda del objeto de escucha de clics en onCreateView() del SleepTrackerFragment para pasar el nightId al SleepTrackerViewModel y activar la navegación al SleepDetailFragment.

Define la función de controlador de clics:

  1. Abre SleepTrackerViewModel.kt.
  2. Dentro de SleepTrackerViewModel, hacia el final, define la función del controlador de clics onSleepNightClicked().
fun onSleepNightClicked(id: Long) {

}
  1. Dentro de onSleepNightClicked(), activa la navegación configurando _navigateToSleepDetail en el id pasado de la noche de sueño en la que se hizo clic.
fun onSleepNightClicked(id: Long) {
   _navigateToSleepDetail.value = id
}
  1. Implementar _navigateToSleepDetail Como ya hiciste, define un private MutableLiveData para el estado de navegación. Y un val público para acompañarlo.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
   get() = _navigateToSleepDetail
  1. Define el método que se llamará después de que la app termine de navegar. Llámala onSleepDetailNavigated() y establece su valor en null.
fun onSleepDetailNavigated() {
    _navigateToSleepDetail.value = null
}

Agrega el código para llamar al controlador de clics:

  1. Abre SleepTrackerFragment.kt y desplázate hacia abajo hasta el código que crea el adaptador y define SleepNightListener para mostrar un mensaje Toast.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
   Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})
  1. Agrega el siguiente código debajo del mensaje emergente para llamar a un controlador de clics, onSleepNighClicked(), en el sleepTrackerViewModel cuando se presiona un elemento. Pasa el nightId para que el modelo de vista sepa qué noche de sueño debe obtener. Esto generará un error, ya que aún no definiste onSleepNightClicked(). Puedes conservar, comentar o borrar el mensaje emergente, según lo desees.
sleepTrackerViewModel.onSleepNightClicked(nightId)

Agrega el código para observar los clics:

  1. Abre SleepTrackerFragment.kt.
  2. En onCreateView(), justo encima de la declaración de manager, agrega código para observar el nuevo navigateToSleepDetail LiveData. Cuando cambia navigateToSleepDetail, navega a SleepDetailFragment, pasa night y, luego, llama a onSleepDetailNavigated(). Como ya lo hiciste en un codelab anterior, aquí tienes el código:
sleepTrackerViewModel.navigateToSleepDetail.observe(this, Observer { night ->
            night?.let {
              this.findNavController().navigate(
                        SleepTrackerFragmentDirections
                                .actionSleepTrackerFragmentToSleepDetailFragment(night))
               sleepTrackerViewModel.onSleepDetailNavigated()
            }
        })
  1. Ejecuta el código, haz clic en un elemento y… la app falla.

Controla los valores nulos en los adaptadores de vinculación:

  1. Vuelve a ejecutar la app en modo de depuración. Presiona un elemento y filtra los registros para mostrar los errores. Se mostrará un seguimiento de pila que incluirá algo similar a lo siguiente.
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull, parameter item

Lamentablemente, el seguimiento de pila no indica con claridad dónde se activa este error. Una desventaja de la vinculación de datos es que puede dificultar la depuración del código. La app falla cuando haces clic en un elemento, y el único código nuevo es para controlar el clic.

Sin embargo, resulta que, con este nuevo mecanismo de control de clics, ahora es posible que se llame a los adaptadores de vinculación con un valor null para item. En particular, cuando se inicia la app, el LiveData comienza como null, por lo que debes agregar verificaciones de nulos a cada uno de los adaptadores.

  1. En BindingUtils.kt, para cada uno de los adaptadores de vinculación, cambia el tipo del argumento item a anulable y ajusta el cuerpo con item?.let{...}. Por ejemplo, tu adaptador para sleepQualityString se verá de la siguiente manera. Cambia los demás adaptadores de la misma manera.
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight?) {
   item?.let {
       text = convertNumericQualityToString(item.sleepQuality, context.resources)
   }
}
  1. Ejecuta tu app. Presiona un elemento y se abrirá una vista de detalles.

Proyecto de Android Studio: RecyclerViewClickHandler

Para que los elementos de un RecyclerView respondan a los clics, adjunta objetos de escucha de clics a los elementos de la lista en el ViewHolder y controla los clics en el ViewModel.

Para que los elementos de un RecyclerView respondan a los clics, debes hacer lo siguiente:

  • Crea una clase de escucha que tome una lambda y la asigne a una función onClick().
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
   fun onClick(night: SleepNight) = clickListener(night.nightId)
}
  • Configura el objeto de escucha de clics en la vista.
android:onClick="@{() -> clickListener.onClick(sleep)}"
  • Pasa el objeto de escucha de clics al constructor del adaptador, al titular de la vista y agrégalo al objeto de vinculación.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()
holder.bind(getItem(position)!!, clickListener)
binding.clickListener = clickListener
  • En el fragmento que muestra la vista de reciclador, donde creas el adaptador, define un objeto de escucha de clics pasando una expresión lambda al adaptador.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
      sleepTrackerViewModel.onSleepNightClicked(nightId)
})
  • Implementa el controlador de clics en el ViewModel. En el caso de los clics en elementos de la lista, esto suele activar la navegación a un fragmento de detalles.

Curso de Udacity:

Documentación para desarrolladores de Android:

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

Supongamos que tu app contiene un RecyclerView que muestra elementos en una lista de compras. Tu app también define una clase de objeto de escucha de clics:

class ShoppingListItemListener(val clickListener: (itemId: Long) -> Unit) {
    fun onClick(cartItem: CartItem) = clickListener(cartItem.itemId)
}

¿Cómo haces que ShoppingListItemListener esté disponible para la vinculación de datos? Selecciona una opción.

▢ En el archivo de diseño que contiene el RecyclerView que muestra la lista de compras, agrega una variable <data> para ShoppingListItemListener.

▢ En el archivo de diseño que define el diseño de una sola fila en la lista de compras, agrega una variable <data> para ShoppingListItemListener.

▢ En la clase ShoppingListItemListener, agrega una función para habilitar la vinculación de datos:

fun onBinding (cartItem: CartItem) {dataBindingEnable(true)}

▢ En la clase ShoppingListItemListener, dentro de la función onClick(), agrega una llamada para habilitar la vinculación de datos:

fun onClick(cartItem: CartItem) = { 
    clickListener(cartItem.itemId)
    dataBindingEnable(true)
}

Pregunta 2

¿Dónde se debe agregar el atributo android:onClick para que los elementos de una RecyclerView respondan a los clics? Selecciona todas las opciones que correspondan.

▢ En el archivo de diseño que muestra RecyclerView, agrégalo a <androidx.recyclerview.widget.RecyclerView>

▢ Se debe agregar al archivo de diseño de un elemento de la fila. Si quieres que se pueda hacer clic en todo el elemento, agrégalo a la vista superior que contiene los elementos de la fila.

▢ Se debe agregar al archivo de diseño de un elemento de la fila. Si quieres que se pueda hacer clic en una sola TextView en el elemento, agrégala a <TextView>.

▢ Siempre se debe agregar al archivo de diseño para MainActivity.

Comienza la próxima lección: 7.5: Encabezados en RecyclerView