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
safeArgspara 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
RecyclerViewbásico con unAdapter, unViewHoldery 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
RecyclerViewadmitan 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
- Descarga el código de inicio de RecyclerViewClickHandler desde GitHub y abre el proyecto en Android Studio.
- 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.
- 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.
- Copia todos los archivos del paquete
sleepdetail. - En la carpeta
layout, copia el archivofragment_sleep_detail.xml. - Copia el contenido actualizado de
navigation.xml, que agrega la navegación parasleep_detail_fragment. - En el paquete
database, enSleepDatabaseDao, agrega el nuevo métodogetNightWithId():
/**
* Selects and returns the night with given nightId.
*/
@Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
fun getNightWithId(key: Long): LiveData<SleepNight>- En
res/values/strings, agrega el siguiente recurso de cadena:
<string name="close">Close</string>- 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:
- 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. - En el paquete
sleepdetail, abre y revisa el código deSleepDetailViewModel. Este modelo de vista toma la clave para unSleepNighty un DAO en el constructor.
El cuerpo de la clase tiene código para obtener elSleepNightpara la clave determinada y la variablenavigateToSleepTrackerpara controlar la navegación de vuelta aSleepTrackerFragmentcuando se presiona el botón Cerrar.
La funcióngetNightWithId()devuelve unLiveData<SleepNight>y se define enSleepDatabaseDao(en el paquetedatabase). - En el paquete
sleepdetail, abre y revisa el código deSleepDetailFragment. Observa la configuración de la vinculación de datos, el modelo de vista y el observador para la navegación. - En el paquete
sleepdetail, abre e inspecciona el código deSleepDetailViewModelFactory. - En la carpeta de diseño, inspecciona
fragment_sleep_detail.xml. Observa la variablesleepDetailViewModeldefinida en la etiqueta<data>para obtener los datos que se mostrarán en cada vista del modelo de vista.
El diseño contiene unConstraintLayoutque incluye unImageViewpara la calidad del sueño, unTextViewpara la calificación de calidad, unTextViewpara la duración del sueño y unButtonpara cerrar el fragmento de detalles. - Abre el archivo
navigation.xml. En elsleep_tracker_fragment, observa la nueva acción para elsleep_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
SleepTrackerFragmentaloja 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
Adaptermuestra 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
- En la carpeta
sleeptracker, abre SleepNightAdapter.kt. - Al final del archivo, en el nivel superior, crea una nueva clase de escucha,
SleepNightListener.
class SleepNightListener() {
}- Dentro de la clase
SleepNightListener, agrega una funciónonClick(). Cuando se hace clic en la vista que muestra un elemento de la lista, la vista llama a esta funciónonClick(). (Más adelante, establecerás la propiedadandroid:onClickde la vista en esta función).
class SleepNightListener() {
fun onClick() =
}- Agrega un argumento de función
nightde tipoSleepNightaonClick(). La vista sabe qué elemento muestra, y esa información debe pasarse para controlar el clic.
class SleepNightListener() {
fun onClick(night: SleepNight) =
}- Para definir lo que hace
onClick(), proporciona una devolución de llamadaclickListeneren el constructor deSleepNightListenery asígnala aonClick().
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 llamadaclickListenersolo necesita elnight.nightIdpara acceder a los datos de la base de datos. La claseSleepNightListenerterminada debería tener el siguiente aspecto:
class SleepNightListener(val clickListener: (sleepId: Long) -> Unit) {
fun onClick(night: SleepNight) = clickListener(night.nightId)
}- Abre list_item_sleep_night.xml.
- Dentro del bloque
data, agrega una variable nueva para que la claseSleepNightListeneresté disponible a través de la vinculación de datos. Asigna al nuevo<variable>unnamedeclickListener.. Configuratypecomo el nombre completamente calificado de la clasecom.example.android.trackmysleepquality.sleeptracker.SleepNightListener, como se muestra a continuación. Ahora puedes acceder a la funciónonClick()enSleepNightListenerdesde este diseño.
<variable
name="clickListener"
type="com.example.android.trackmysleepquality.sleeptracker.SleepNightListener" />- Para detectar clics en cualquier parte de este elemento de lista, agrega el atributo
android:onClickalConstraintLayout.
Establece el atributo enclickListener: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
- Abre SleepNightAdapter.kt.
- Modifica el constructor de la clase
SleepNightAdapterpara que reciba unval clickListener: SleepNightListener. Cuando el adaptador vincule elViewHolder, deberá proporcionarle este objeto de escucha de clics.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {- En
onBindViewHolder(), actualiza la llamada aholder.bind()para que también pase el objeto de escucha de clics alViewHolder. Obtendrás un error del compilador porque agregaste un parámetro a la llamada a la función.
holder.bind(getItem(position)!!, clickListener)- Agrega el parámetro
clickListenerabind(). Para ello, coloca el cursor sobre el error y presionaAlt+Enter(Windows) oOption+Enter(Mac) sobre el error, como se muestra en la siguiente captura de pantalla.
- Dentro de la clase
ViewHolder, dentro de la funciónbind(), asigna el objeto de escucha de clics al objetobinding. Verás un error porque debes actualizar el objeto de vinculación.
binding.clickListener = clickListener- 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.
- Abre SleepTrackerFragment.kt.
- En
onCreateView(), busca la variableadapter. Observa que muestra un error, ya que ahora espera un parámetro de objeto de escucha de clics. - Define un objeto de escucha de clics pasando una lambda al
SleepNightAdapter. Esta lambda simple solo muestra un aviso que muestra elnightId, como se muestra a continuación. Deberás importarToast. A continuación, se muestra la definición actualizada completa.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})- Ejecuta la app, presiona elementos y verifica que muestren un mensaje emergente con el
nightIdcorrecto. Dado que los elementos tienen valores denightIdcrecientes y la app muestra la noche más reciente primero, el elemento con el valor denightIdmá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:
- Abre SleepTrackerViewModel.kt.
- Dentro de
SleepTrackerViewModel, hacia el final, define la función del controlador de clicsonSleepNightClicked().
fun onSleepNightClicked(id: Long) {
}- Dentro de
onSleepNightClicked(), activa la navegación configurando_navigateToSleepDetailen elidpasado de la noche de sueño en la que se hizo clic.
fun onSleepNightClicked(id: Long) {
_navigateToSleepDetail.value = id
}- Implementar
_navigateToSleepDetailComo ya hiciste, define unprivate MutableLiveDatapara el estado de navegación. Y unvalpúblico para acompañarlo.
private val _navigateToSleepDetail = MutableLiveData<Long>()
val navigateToSleepDetail
get() = _navigateToSleepDetail- Define el método que se llamará después de que la app termine de navegar. Llámala
onSleepDetailNavigated()y establece su valor ennull.
fun onSleepDetailNavigated() {
_navigateToSleepDetail.value = null
}Agrega el código para llamar al controlador de clics:
- Abre SleepTrackerFragment.kt y desplázate hacia abajo hasta el código que crea el adaptador y define
SleepNightListenerpara mostrar un mensaje Toast.
val adapter = SleepNightAdapter(SleepNightListener { nightId ->
Toast.makeText(context, "${nightId}", Toast.LENGTH_LONG).show()
})- Agrega el siguiente código debajo del mensaje emergente para llamar a un controlador de clics,
onSleepNighClicked(), en elsleepTrackerViewModelcuando se presiona un elemento. Pasa elnightIdpara que el modelo de vista sepa qué noche de sueño debe obtener. Esto generará un error, ya que aún no definisteonSleepNightClicked(). Puedes conservar, comentar o borrar el mensaje emergente, según lo desees.
sleepTrackerViewModel.onSleepNightClicked(nightId)Agrega el código para observar los clics:
- Abre SleepTrackerFragment.kt.
- En
onCreateView(), justo encima de la declaración demanager, agrega código para observar el nuevonavigateToSleepDetailLiveData. Cuando cambianavigateToSleepDetail, navega aSleepDetailFragment, pasanighty, luego, llama aonSleepDetailNavigated(). 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()
}
})- Ejecuta el código, haz clic en un elemento y… la app falla.
Controla los valores nulos en los adaptadores de vinculación:
- 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 itemLamentablemente, 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.
- En
BindingUtils.kt, para cada uno de los adaptadores de vinculación, cambia el tipo del argumentoitema anulable y ajusta el cuerpo conitem?.let{...}. Por ejemplo, tu adaptador parasleepQualityStringse 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)
}
}- 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:

