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 unAdapter
, unViewHolder
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
- 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 unSleepNight
y un DAO en el constructor.
El cuerpo de la clase tiene código para obtener elSleepNight
para la clave determinada y la variablenavigateToSleepTracker
para controlar la navegación de vuelta aSleepTrackerFragment
cuando 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 variablesleepDetailViewModel
definida en la etiqueta<data>
para obtener los datos que se mostrarán en cada vista del modelo de vista.
El diseño contiene unConstraintLayout
que incluye unImageView
para la calidad del sueño, unTextView
para la calificación de calidad, unTextView
para la duración del sueño y unButton
para 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
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
- 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:onClick
de la vista en esta función).
class SleepNightListener() {
fun onClick() =
}
- Agrega un argumento de función
night
de tipoSleepNight
aonClick()
. 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 llamadaclickListener
en el constructor deSleepNightListener
y 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 llamadaclickListener
solo necesita elnight.nightId
para acceder a los datos de la base de datos. La claseSleepNightListener
terminada 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 claseSleepNightListener
esté disponible a través de la vinculación de datos. Asigna al nuevo<variable>
unname
declickListener.
. Configuratype
como 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()
enSleepNightListener
desde 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:onClick
alConstraintLayout
.
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
SleepNightAdapter
para 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
clickListener
abind()
. 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
nightId
correcto. Dado que los elementos tienen valores denightId
crecientes y la app muestra la noche más reciente primero, el elemento con el valor denightId
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:
- 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_navigateToSleepDetail
en elid
pasado de la noche de sueño en la que se hizo clic.
fun onSleepNightClicked(id: Long) {
_navigateToSleepDetail.value = id
}
- Implementar
_navigateToSleepDetail
Como ya hiciste, define unprivate MutableLiveData
para el estado de navegación. Y unval
pú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
SleepNightListener
para 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 elsleepTrackerViewModel
cuando se presiona un elemento. Pasa elnightId
para 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 nuevonavigateToSleepDetail
LiveData
. Cuando cambianavigateToSleepDetail
, navega aSleepDetailFragment
, pasanight
y, 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 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.
- En
BindingUtils.kt
, para cada uno de los adaptadores de vinculación, cambia el tipo del argumentoitem
a anulable y ajusta el cuerpo conitem?.let{...}
. Por ejemplo, tu adaptador parasleepQualityString
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)
}
}
- 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: