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 en secuencia. Todos los codelabs del curso se detallan en la página de destino de codelabs sobre los aspectos básicos de Kotlin para Android.
Introducción
En este codelab, aprenderás a agregar un encabezado que abarque el ancho de la lista que se muestra en un elemento RecyclerView
. Compilarás en la app de seguimiento de sueño a partir de 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 - Visualiza 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 las interacciones de bases de datos y otras tareas de larga duración
- Cómo implementar un objeto
RecyclerView
básico con un elementoAdapter
,ViewHolder
y diseño de elementos - 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 captar y controlar clics en elementos en una
RecyclerView.
Qué aprenderás
- Cómo usar más de un
ViewHolder
con unaRecyclerView
a fin de agregar elementos con un diseño diferente En particular, cómo usar un segundo elementoViewHolder
para agregar un encabezado encima de los elementos que se muestran enRecyclerView
.
Actividades
- Compila en 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 el
RecyclerView
.
La app de seguimiento del sueño con la que comienzas 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. La pantalla muestra algunos datos de sueño del usuario. El botón Borrar borra de forma permanente todos los datos que la app recopiló para el 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 en la cuadrícula.
Esta app usa una arquitectura simplificada con un controlador de IU, un modelo de vista 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 una RecyclerView
. Un ejemplo común es tener encabezados en la 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 tiene información sobre tus datos ni el tipo de diseño que tiene cada elemento. El LayoutManager
organiza los elementos en la pantalla, pero el adaptador adapta los datos que se muestran y pasa los contenedores de vistas al 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:
[Datos reales] -> [Vistas del adaptador]
[0: Sueño nocturno] -[0: Sueño]
[1: Sueño nocturno] -> [1: Sueño nocturno]
[2: Sueño nocturno] -[g: SleepSleep]
Una forma de agregar encabezados a una lista es modificar tu adaptador para usar un ViewHolder
diferente verificando los índices en los que se debe mostrar el encabezado. El Adapter
será responsable de realizar el seguimiento del encabezado. Por ejemplo, para mostrar un encabezado en la parte superior de la tabla, debes mostrar un ViewHolder
diferente para el encabezado mientras colocas el elemento con índice cero. Luego, todos los demás elementos se asignarían con la compensación del encabezado, como se muestra a continuación.
[Datos reales] -> [Vistas del adaptador]
[0: Encabezado]
[0: SleepNight] -[1: SleepNight]
[1: Sueño nocturno] -[g: SleepNight]
[2: Sueño nocturno] -> [3: Sueño.
Otra forma de agregar encabezados es modificar el conjunto de datos de respaldo para la cuadrícula de datos. Debido a que todos los datos que se deben mostrar se almacenan en una lista, puede modificarla para que incluya elementos que representen un encabezado. Esto es un poco más fácil de entender, pero requiere que piense en cómo diseña sus objetos, de modo que pueda combinar los diferentes tipos de elementos en una sola lista. Si se implementa de esta manera, el adaptador mostrará los elementos que se le pasaron. 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.
[Datos reales] -> [Vistas del adaptador]
[0: Encabezado] -> [0: Encabezado]
[1: Sueño nocturno] -> [1: Sueño nocturno]
[2: Sueño nocturno] -[g: SleepSleep]
[3: Sueño nocturno] -> [3: Sueño nocturno]
Cada metodología tiene ventajas y desventajas. Cambiar el conjunto de datos no presenta 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, el uso de un ViewHolder
diferente mediante la verificación de índices en los encabezados brinda más libertad sobre 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 copia de seguridad.
En este codelab, actualizarás tu RecyclerView
para mostrar un encabezado al comienzo de la lista. En este caso, la app usará un elemento 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 &items;items, puedes crear una clase de contenedor de datos que represente una SleepNight
o una Header
. Tu conjunto de datos será una lista de elementos que contienen datos.
Puedes obtener la app inicial de GitHub o seguir usando la app de SleepTracker que compilaste en el codelab anterior.
- Descarga el código RecyclerViewHeaders-Starter de 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 terminada del 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. No es posible que otra parte del código defina un nuevo tipo deDataItem
que pueda dañar el 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 unaSleepNight
, por lo que toma un solo valor llamadosleepNight
. Para que sea parte de la clase sellada, haz que extiendaDataItem
.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- La segunda clase es
Header
, que representa un encabezado. Debido a que un encabezado no tiene datos reales, puedes declararlo comoobject
. Esto significa que solo habrá una instancia deHeader
. Una vez más, 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 cambió un elemento y de qué forma, elDiffItemCallback
debe 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 mostrarnightId
.
override val id = sleepNight.nightId
- En
Header
, anulaid
para mostrarLong.MIN_VALUE
, que es un número muy pequeño (literalmente, -2 a la potencia de 63). Por lo tanto, nunca entrará en conflicto con ningúnnightId
.
override val id = Long.MIN_VALUE
- El código finalizado debería verse de la siguiente manera, 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 muestra un
TextView
. No hay nada interesante sobre 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 strings 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 aumenta el diseño de textview.xml y muestra una instancia deTextViewHolder
. Como lo hiciste antes, este es el código, y deberás 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 SleepSleepAdapter
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.
Cómo definir los tipos de elementos
- En
SleepNightAdapter.kt
, en el nivel superior, debajo de las sentenciasimport
y arriba deSleepNightAdapter
, define dos constantes para los tipos de vista.
ElRecyclerView
deberá distinguir cada tipo de vista de cada elemento a fin de poder asignarle correctamente un contenedor de vistas.
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 1
- Dentro del
SleepNightAdapter
, crea una función para anulargetItemViewType()
y mostrar el encabezado o la constante de elemento correctos según el tipo de 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 deListAdapter
deSleepNight
aDataItem
. - En la definición de
SleepNightAdapter
, cambia el segundo argumento genérico paraListAdapter
deSleepNightAdapter.ViewHolder
aRecyclerView.ViewHolder
. Verás algunos errores en 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()) {
Cómo actualizar onCreateViewHolder()
- Cambia la firma de
onCreateViewHolder()
para mostrar unRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- Expande la implementación del método
onCreateViewHolder()
a fin de probar y mostrar el contenedor de vistas adecuado para cada tipo de elemento. El método actualizado debería ser similar al 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}")
}
}
Cómo actualizar 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 solo datos al contenedor de vistas si el contenedor es un
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Transmite el tipo de objeto que muestra
getItem()
aDataItem.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)
}
}
}
Cómo actualizar las devoluciones de llamada de diffUtil
- Cambia los métodos de
SleepNightDiffCallback
para usar la clase nuevaDataItem
en lugar deSleepNight
. Elimina 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
}
}
Agregar y enviar 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()
, que proporcionaListAdapter
, para enviar tu lista, debes usar esta función a fin de agregar un encabezado y, luego, enviar la lista.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- Dentro de
addHeaderAndSubmitList()
, si el pase en la lista esnull
, muestra solo un encabezado. De lo contrario, adjunta el encabezado al encabezado 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 suspensión.
Hay dos aspectos que se deben corregir en esta app. Uno es visible y el otro no.
- El encabezado aparece en la esquina superior izquierda y no se puede distinguir fácilmente.
- No importa mucho para una lista corta con un encabezado, pero no debes realizar la manipulación de listas en
addHeaderAndSubmitList()
del subproceso de IU. Imagina una lista con cientos de elementos, varios encabezados y lógica para decidir dónde insertar los elementos. Este trabajo pertenece a una corrutina.
Cambia addHeaderAndSubmitList()
para usar corrutinas:
- En el nivel superior de la clase
SleepNightAdapter
, define un elementoCoroutineScope
conDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- En
addHeaderAndSubmitList()
, inicia una corrutina en eladapterScope
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 habrá ninguna diferencia.
Actualmente, el encabezado tiene el mismo ancho que los demás elementos de la cuadrícula, y ocupa un intervalo horizontal y verticalmente. La cuadrícula completa se ajusta a tres elementos de un ancho de intervalo horizontalmente, por lo que el encabezado debe usar tres intervalos horizontalmente.
Para corregir el ancho del encabezado, debes indicarle a GridLayoutManager
cuándo abarcar los datos en todas las columnas. Para ello, configura el SpanSizeLookup
en un GridLayoutManager
. Este es un objeto de configuración que GridLayoutManager
usa a fin de determinar cuántos intervalos usar para cada elemento de la lista.
- Abre SleepTrackerFragment.kt.
- Busca el código en el que defines
manager
hacia el final deonCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Debajo de
manager
, definemanager.spanSizeLookup
, como se muestra. Debes realizar unaobject
porquesetSpanSizeLookup
no toma una expresión lambda. Para hacer unaobject
en Kotlin, escribeobject : classname
, en este casoGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- Es posible que recibas un error de compilador para llamar al constructor. Si lo haces, abre el menú de intents con
Option+Enter
(Mac) oAlt+Enter
(Windows) para aplicar la llamada al constructor.
- Luego, verá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()
, muestra 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 de la siguiente manera:
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
- Por lo general, un encabezado es 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 de un elemento o varios encabezados para agrupar los elementos y separarlos entre sí.
- Un objeto
RecyclerView
puede usar varios contenedores de vistas para alojar un conjunto heterogéneo de elementos; por ejemplo, encabezados y elementos de listas. - Una forma de agregar encabezados es modificar tu adaptador para usar un
ViewHolder
diferente verificando los índices en los que se debe mostrar el encabezado.Adapter
es responsable de realizar 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:
- Para abstraer los datos de tu lista, crea un
DataItem
que pueda contener un encabezado o datos. - Crea un contenedor de vistas con un diseño para el encabezado del adaptador.
- Actualiza el adaptador y sus métodos para usar cualquier tipo de
RecyclerView.ViewHolder
. - En
onCreateViewHolder()
, muestra el tipo de contenedor de vistas correcto 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 intervalos.
Curso de Udacity:
Documentación para desarrolladores de Android:
En esta sección, se enumeran las posibles tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:
- Si es necesario, asigna la tarea.
- Informa a los alumnos cómo enviar los deberes.
- Califica las tareas.
Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.
Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.
Responde estas preguntas
Pregunta 1
¿Cuál de las siguientes afirmaciones es verdadera sobre ViewHolder
?
▢ Un adaptador puede usar varias clases ViewHolder
para contener encabezados y varios tipos de datos.
▢ Puedes tener exactamente un contenedor de vistas para los datos y un contenedor de vistas para un encabezado.
▢ Un RecyclerView
admite varios tipos de encabezados, pero los datos deben ser uniformes.
▢ Cuando agrega un encabezado, puede crear una subclase RecyclerView
para insertarlo en la posición correcta.
Pregunta 2
¿Cuándo deberías usar corrutinas con un RecyclerView
? Seleccione 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.
▢ La manipulación de listas puede llevar mucho tiempo y siempre se deben hacer mediante corrutinas.
▢ Usa corrutinas con funciones de suspensión para no bloquear el subproceso principal.
Pregunta 3
¿Cuál de las siguientes acciones NO debes realizar cuando usas más de un ViewHolder
?
▢ En ViewHolder
, proporciona varios archivos de diseño para aumentarlos según sea necesario.
▢ En onCreateViewHolder()
, muestra el tipo correcto de contenedor de vistas para el elemento de datos.
▢ En onBindViewHolder()
, solo vincula datos si el contenedor de vistas es el tipo correcto para el elemento de datos.
▢ Generaliza la firma de clase del adaptador para aceptar cualquier RecyclerView.ViewHolder
.
Comience la siguiente lección:
Para ver vínculos a otros codelabs de este curso, consulta la página de destino de codelabs sobre aspectos básicos de Kotlin para Android.