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
En este codelab, aprenderás a agregar un encabezado que abarque el ancho de la lista que se muestra en un RecyclerView
. Compilarás la app de Sleep Tracker de los codelabs anteriores.
Conocimientos que ya deberías tener
- Cómo compilar una interfaz de usuario básica con una actividad, fragmentos y vistas
- Cómo navegar entre fragmentos y cómo 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 DAO y definir entidades - Cómo usar corrutinas para interacciones con 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
- Cómo capturar y controlar los clics en elementos de un
RecyclerView.
Qué aprenderás
- Cómo usar más de un
ViewHolder
con unRecyclerView
para agregar elementos con un diseño diferente Específicamente, cómo usar un segundoViewHolder
para agregar un encabezado sobre los elementos que se muestran enRecyclerView
.
Actividades
- Compilar la app de TrackMySleepQuality del codelab anterior de esta serie
- Agrega un encabezado que abarque el ancho de la pantalla sobre las noches de sueño que se muestran en
RecyclerView
.
La app de monitoreo del sueño con la que comenzarás tiene tres 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 en el medio, es para seleccionar una calificación de calidad del sueño. La tercera pantalla es una vista de detalles que se abre cuando el usuario presiona un elemento de la cuadrícula.
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 un encabezado a la cuadrícula de elementos que se muestra. La pantalla principal final se verá de la siguiente manera:
En este codelab, se enseña el principio general de incluir elementos que usan diferentes diseños en un RecyclerView
. Un ejemplo común es tener encabezados en tu lista o cuadrícula. Una lista puede tener un solo encabezado para describir el contenido del elemento. Una lista también puede tener varios encabezados para agrupar y separar elementos en una sola lista.
RecyclerView
no sabe nada sobre tus datos ni qué tipo de diseño tiene cada elemento. LayoutManager
organiza los elementos en la pantalla, pero el adaptador adapta los datos que se mostrarán y pasa los contenedores de vistas a RecyclerView
. Por lo tanto, agregarás el código para crear encabezados en el adaptador.
Dos formas de agregar encabezados
En RecyclerView
, cada elemento de la lista corresponde a un número de índice que comienza en 0. Por ejemplo:
[Actual Data] -> [Adapter Views]
[0: SleepNight] -> [0: SleepNight]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
Una forma de agregar encabezados a una lista es modificar el adaptador para que use un ViewHolder
diferente. Para ello, verifica los índices en los que se debe mostrar el encabezado. El Adapter
será responsable de hacer un seguimiento del encabezado. Por ejemplo, para mostrar un encabezado en la parte superior de la tabla, debes devolver un ViewHolder
diferente para el encabezado mientras diseñas el elemento indexado en cero. Luego, todos los demás elementos se correlacionarían con el desplazamiento del encabezado, como se muestra a continuación.
[Actual Data] -> [Adapter Views]
[0: Header]
[0: SleepNight] -> [1: SleepNight]
[1: SleepNight] -> [2: SleepNight]
[2: SleepNight] -> [3: SleepNight.
Otra forma de agregar encabezados es modificar el conjunto de datos de respaldo de tu cuadrícula de datos. Dado que todos los datos que se deben mostrar se almacenan en una lista, puedes modificarla para incluir elementos que representen un encabezado. Esto es un poco más fácil de entender, pero requiere que pienses en cómo diseñar tus objetos para que puedas combinar los diferentes tipos de elementos en una sola lista. De esta manera, el adaptador mostrará los elementos que se le pasen. Por lo tanto, el elemento en la posición 0 es un encabezado, y el elemento en la posición 1 es un SleepNight
, que se asigna directamente a lo que se muestra en la pantalla.
[Actual Data] -> [Adapter Views]
[0: Header] -> [0: Header]
[1: SleepNight] -> [1: SleepNight]
[2: SleepNight] -> [2: SleepNight]
[3: SleepNight] -> [3: SleepNight]
Cada metodología tiene sus ventajas y desventajas. Cambiar el conjunto de datos no introduce muchos cambios en el resto del código del adaptador, y puedes agregar lógica de encabezado manipulando la lista de datos. Por otro lado, usar un ViewHolder
diferente verificando los índices de los encabezados brinda más libertad en el diseño del encabezado. También permite que el adaptador controle cómo se adaptan los datos a la vista sin modificar los datos de respaldo.
En este codelab, actualizarás tu RecyclerView
para mostrar un encabezado al comienzo de la lista. En este caso, tu app usará un ViewHolder
diferente para el encabezado que para los elementos de datos. La app verificará el índice de la lista para determinar qué ViewHolder
usar.
Paso 1: Crea una clase DataItem
Para abstraer el tipo de elemento y permitir que el adaptador solo se encargue de los "elementos", puedes crear una clase de contenedor de datos que represente un SleepNight
o un Header
. Tu conjunto de datos será una lista de elementos de titulares de datos.
Puedes obtener la app de partida desde GitHub o seguir usando la app de SleepTracker que compilaste en el codelab anterior.
- Descarga el código de RecyclerViewHeaders-Starter desde GitHub. El directorio RecyclerViewHeaders-Starter contiene la versión inicial de la app de SleepTracker que se necesita para este codelab. Si lo prefieres, también puedes continuar con la app que completaste en el codelab anterior.
- Abre SleepNightAdapter.kt.
- Debajo de la clase
SleepNightListener
, en el nivel superior, define una clasesealed
llamadaDataItem
que represente un elemento de datos.
Una clasesealed
define un tipo cerrado, lo que significa que todas las subclases deDataItem
deben definirse en este archivo. Como resultado, el compilador conoce la cantidad de subclases. Otra parte de tu código no puede definir un nuevo tipo deDataItem
que podría dañar tu adaptador.
sealed class DataItem {
}
- Dentro del cuerpo de la clase
DataItem
, define dos clases que representen los diferentes tipos de elementos de datos. El primero es unSleepNightItem
, que es un wrapper alrededor de unSleepNight
, por lo que toma un solo valor llamadosleepNight
. Para que forme parte de la clase sellada, haz que extiendaDataItem
.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- La segunda clase es
Header
, para representar un encabezado. Dado que un encabezado no tiene datos reales, puedes declararlo como unobject
. Esto significa que solo habrá una instancia deHeader
. Nuevamente, haz que extiendaDataItem
.
object Header: DataItem()
- Dentro de
DataItem
, a nivel de la clase, define una propiedadabstract
Long
llamadaid
. Cuando el adaptador usaDiffUtil
para determinar si un elemento cambió y cómo lo hizo, elDiffItemCallback
necesita conocer el ID de cada elemento. Verás un error porqueSleepNightItem
yHeader
deben anular la propiedad abstractaid
.
abstract val id: Long
- En
SleepNightItem
, anulaid
para devolvernightId
.
override val id = sleepNight.nightId
- En
Header
, anulaid
para devolverLong.MIN_VALUE
, que es un número muy, muy pequeño (literalmente, -2 elevado a la potencia de 63). Por lo tanto, nunca entrará en conflicto con ningúnnightId
existente.
override val id = Long.MIN_VALUE
- El código finalizado debería verse así, y la app debería compilarse sin errores.
sealed class DataItem {
abstract val id: Long
data class SleepNightItem(val sleepNight: SleepNight): DataItem() {
override val id = sleepNight.nightId
}
object Header: DataItem() {
override val id = Long.MIN_VALUE
}
}
Paso 2: Crea un ViewHolder para el encabezado
- Crea el diseño del encabezado en un nuevo archivo de recursos de diseño llamado header.xml que muestre un
TextView
. No hay nada emocionante en esto, así que aquí está el código.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Sleep Results"
android:padding="8dp" />
- Extrae
"Sleep Results"
en un recurso de cadena y llámaloheader_text
.
<string name="header_text">Sleep Results</string>
- En SleepNightAdapter.kt, dentro de
SleepNightAdapter
, sobre la claseViewHolder
, crea una nueva claseTextViewHolder
. Esta clase infla el diseño textview.xml y devuelve una instancia deTextViewHolder
. Como ya lo hiciste antes, aquí tienes el código y tendrás que importarView
yR
:
class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
companion object {
fun from(parent: ViewGroup): TextViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.header, parent, false)
return TextViewHolder(view)
}
}
}
Paso 3: Actualiza SleepNightAdapter
A continuación, debes actualizar la declaración de SleepNightAdapter
. En lugar de admitir solo un tipo de ViewHolder
, debe poder usar cualquier tipo de contenedor de vistas.
Define los tipos de elementos
- En
SleepNightAdapter.kt
, en el nivel superior, debajo de las instruccionesimport
y arriba deSleepNightAdapter
, define dos constantes para los tipos de vistas.
ElRecyclerView
deberá distinguir el tipo de vista de cada elemento para poder asignarle correctamente un elemento de ViewHolder.
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 1
- Dentro de
SleepNightAdapter
, crea una función para anulargetItemViewType()
y devolver la constante de encabezado o elemento correcta según el tipo del elemento actual.
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
}
}
Actualiza la definición de SleepNightAdapter
- En la definición de
SleepNightAdapter
, actualiza el primer argumento paraListAdapter
deSleepNight
aDataItem
. - En la definición de
SleepNightAdapter
, cambia el segundo argumento genérico paraListAdapter
deSleepNightAdapter.ViewHolder
aRecyclerView.ViewHolder
. Verás algunos errores relacionados con las actualizaciones necesarias, y el encabezado de la clase debería verse como se muestra a continuación.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {
Actualiza onCreateViewHolder()
- Cambia la firma de
onCreateViewHolder()
para que muestre unRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- Expande la implementación del método
onCreateViewHolder()
para probar y devolver el elemento adecuado para cada tipo de elemento. Tu método actualizado debería verse como el siguiente código.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
else -> throw ClassCastException("Unknown viewType ${viewType}")
}
}
Actualiza onBindViewHolder()
- Cambia el tipo de parámetro de
onBindViewHolder()
deViewHolder
aRecyclerView.ViewHolder
.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- Agrega una condición para asignar datos al titular de la vista solo si el titular es un
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Convierte el tipo de objeto que muestra
getItem()
enDataItem.SleepNightItem
. La funciónonBindViewHolder()
terminada debería verse de la siguiente manera:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val nightItem = getItem(position) as DataItem.SleepNightItem
holder.bind(nightItem.sleepNight, clickListener)
}
}
}
Actualiza las devoluciones de llamada de DiffUtil
- Cambia los métodos en
SleepNightDiffCallback
para usar tu nueva claseDataItem
en lugar deSleepNight
. Suprime la advertencia de lint como se muestra en el siguiente código.
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
Agrega y envía el encabezado
- Dentro de
SleepNightAdapter
, debajo deonCreateViewHolder()
, define una funciónaddHeaderAndSubmitList()
como se muestra a continuación. Esta función toma una lista deSleepNight
. En lugar de usarsubmitList()
, proporcionado porListAdapter
, para enviar tu lista, usarás esta función para agregar un encabezado y, luego, enviar la lista.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- Dentro de
addHeaderAndSubmitList()
, si la lista pasada esnull
, devuelve solo un encabezado. De lo contrario, adjunta el encabezado al principio de la lista y, luego, envía la lista.
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
submitList(items)
- Abre SleepTrackerFragment.kt y cambia la llamada a
submitList()
poraddHeaderAndSubmitList()
.
- Ejecuta tu app y observa cómo se muestra el encabezado como el primer elemento de la lista de elementos de sueño.
Hay dos cosas que se deben corregir en esta app. Una es visible y la otra no.
- El encabezado aparece en la esquina superior izquierda y no se distingue fácilmente.
- No importa mucho para una lista corta con un encabezado, pero no debes manipular la lista en
addHeaderAndSubmitList()
en el subproceso de IU. Imagina una lista con cientos de elementos, varios encabezados y lógica para decidir dónde se deben insertar los elementos. Este trabajo pertenece a una corrutina.
Cambia addHeaderAndSubmitList()
para usar corrutinas:
- En el nivel superior dentro de la clase
SleepNightAdapter
, define unCoroutineScope
conDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- En
addHeaderAndSubmitList()
, inicia una corrutina enadapterScope
para manipular la lista. Luego, cambia al contextoDispatchers.Main
para enviar la lista, como se muestra en el siguiente código.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {
adapterScope.launch {
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
withContext(Dispatchers.Main) {
submitList(items)
}
}
}
- Tu código debería compilarse y ejecutarse, y no verás ninguna diferencia.
Actualmente, el encabezado tiene el mismo ancho que los demás elementos de la cuadrícula, y ocupa un tramo horizontal y vertical. Toda la cuadrícula admite tres elementos de un ancho de tramo horizontalmente, por lo que el encabezado debe usar tres tramos horizontalmente.
Para corregir el ancho del encabezado, debes indicarle a GridLayoutManager
cuándo abarcar los datos en todas las columnas. Para ello, configura SpanSizeLookup
en un GridLayoutManager
. Es un objeto de configuración que usa GridLayoutManager
para determinar cuántos tramos usar para cada elemento de la lista.
- Abre SleepTrackerFragment.kt.
- Busca el código en el que defines
manager
, cerca del final deonCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Debajo de
manager
, definemanager.spanSizeLookup
, como se muestra. Debes crear unobject
porquesetSpanSizeLookup
no acepta una lambda. Para crear unobject
en Kotlin, escribeobject : classname
, en este casoGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- Es posible que recibas un error del compilador para llamar al constructor. Si lo haces, abre el menú de intención con
Option+Enter
(Mac) oAlt+Enter
(Windows) para aplicar la llamada al constructor.
- Luego, recibirás un error en
object
que indica que debes anular los métodos. Coloca el cursor enobject
, presionaOption+Enter
(Mac) oAlt+Enter
(Windows) para abrir el menú de intenciones y, luego, anula el métodogetSpanSize()
.
- En el cuerpo de
getSpanSize()
, devuelve el tamaño de intervalo correcto para cada posición. La posición 0 tiene un tamaño de intervalo de 3, y las demás posiciones tienen un tamaño de intervalo de 1. El código completado debería verse como el siguiente:
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = when (position) {
0 -> 3
else -> 1
}
}
- Para mejorar el aspecto del encabezado, abre header.xml y agrega este código al archivo de diseño header.xml.
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
- Ejecuta la app. Debería verse como en la siguiente captura de pantalla.
¡Felicitaciones! Terminaste.
Proyecto de Android Studio: RecyclerViewHeaders
- Un encabezado suele ser un elemento que abarca el ancho de una lista y actúa como título o separador. Una lista puede tener un solo encabezado para describir el contenido del elemento o varios encabezados para agrupar los elementos y separarlos entre sí.
- Un
RecyclerView
puede usar varios contenedores de vistas para admitir un conjunto heterogéneo de elementos, por ejemplo, encabezados y elementos de lista. - Una forma de agregar encabezados es modificar tu adaptador para que use un
ViewHolder
diferente. Para ello, verifica los índices en los que se debe mostrar el encabezado. ElAdapter
es responsable de hacer un seguimiento del encabezado. - Otra forma de agregar encabezados es modificar el conjunto de datos de respaldo (la lista) de tu cuadrícula de datos, que es lo que hiciste en este codelab.
Estos son los pasos principales para agregar un encabezado:
- Abstrae los datos de tu lista creando un
DataItem
que pueda contener un encabezado o datos. - Crea un contenedor de vistas con un diseño para el encabezado en el adaptador.
- Actualiza el adaptador y sus métodos para usar cualquier tipo de
RecyclerView.ViewHolder
. - En
onCreateViewHolder()
, devuelve el tipo correcto de titular de la vista para el elemento de datos. - Actualiza
SleepNightDiffCallback
para que funcione con la claseDataItem
. - Crea una función
addHeaderAndSubmitList()
que use corrutinas para agregar el encabezado al conjunto de datos y, luego, llame asubmitList()
. - Implementa
GridLayoutManager.SpanSizeLookup()
para que solo el encabezado tenga tres tramos de ancho.
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
¿Cuál de las siguientes afirmaciones es verdadera con respecto a ViewHolder
?
▢ Un adaptador puede usar varias clases ViewHolder
para contener encabezados y diferentes tipos de datos.
▢ Se pueden tener exactamente un contenedor de vistas para los datos y uno para un encabezado.
▢ Un objeto RecyclerView
admite varios tipos de encabezados, pero los datos deben ser uniformes.
▢ Cuando se agrega un encabezado, se implementa RecyclerView
como subclase para insertar el encabezado en la posición correcta.
Pregunta 2
¿Cuándo deberías usar corrutinas con un RecyclerView
? Selecciona todas las afirmaciones que sean verdaderas.
▢ Nunca. Un RecyclerView
es un elemento de la IU y no debe usar corrutinas.
▢ Usa corrutinas para tareas de larga duración que podrían ralentizar la IU.
▢ Las manipulaciones de listas pueden tardar mucho tiempo, y siempre debes realizarlas con corrutinas.
▢ Usa corrutinas con funciones de suspensión para evitar bloquear el subproceso principal.
Pregunta 3
¿Cuál de las siguientes opciones NO tienes que hacer cuando usas más de un ViewHolder
?
▢ En el ViewHolder
, proporciona varios archivos de diseño para inflar según sea necesario.
▢ En onCreateViewHolder()
, devuelve el tipo correcto de titular de la vista para el elemento de datos.
▢ En onBindViewHolder()
, solo vincula datos si el titular de la vista es el tipo correcto de titular de la vista para el elemento de datos.
▢ Generaliza la firma de la clase del adaptador para que acepte cualquier RecyclerView.ViewHolder
.
Comienza la siguiente lección:
Para obtener vínculos a otros codelabs de este curso, consulta la página de destino de los codelabs de Conceptos básicos de Kotlin para Android.