Aspectos básicos de Kotlin para Android 07.5: Encabezados en 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 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 elemento Adapter, 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 una RecyclerView a fin de agregar elementos con un diseño diferente En particular, cómo usar un segundo elemento ViewHolder para agregar un encabezado encima de los elementos que se muestran en RecyclerView.

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.

  1. 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.
  2. Abre SleepNightAdapter.kt.
  3. Debajo de la clase SleepNightListener, en el nivel superior, define una clase sealed llamada DataItem que represente un elemento de datos.

    Una clase sealed define un tipo cerrado, lo que significa que todas las subclases de DataItem 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 de DataItem que pueda dañar el adaptador.
sealed class DataItem {

 }
  1. Dentro del cuerpo de la clase DataItem, define dos clases que representen los diferentes tipos de elementos de datos. El primero es un SleepNightItem, que es un wrapper alrededor de una SleepNight, por lo que toma un solo valor llamado sleepNight. Para que sea parte de la clase sellada, haz que extienda DataItem.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. La segunda clase es Header, que representa un encabezado. Debido a que un encabezado no tiene datos reales, puedes declararlo como object. Esto significa que solo habrá una instancia de Header. Una vez más, haz que extienda DataItem.
object Header: DataItem()
  1. Dentro de DataItem, a nivel de la clase, define una propiedad abstract Long llamada id. Cuando el adaptador usa DiffUtil para determinar si cambió un elemento y de qué forma, el DiffItemCallback debe conocer el ID de cada elemento. Verás un error porque SleepNightItem y Header deben anular la propiedad abstracta id.
abstract val id: Long
  1. En SleepNightItem, anula id para mostrar nightId.
override val id = sleepNight.nightId
  1. En Header, anula id para mostrar Long.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ún nightId.
override val id = Long.MIN_VALUE
  1. 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

  1. 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" />
  1. Extrae "Sleep Results" en un recurso de strings y llámalo header_text.
<string name="header_text">Sleep Results</string>
  1. En SleepNightAdapter.kt, dentro de SleepNightAdapter, sobre la clase ViewHolder, crea una nueva clase TextViewHolder. Esta clase aumenta el diseño de textview.xml y muestra una instancia de TextViewHolder. Como lo hiciste antes, este es el código, y deberás importar View y R:
    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

  1. En SleepNightAdapter.kt, en el nivel superior, debajo de las sentencias import y arriba de SleepNightAdapter, define dos constantes para los tipos de vista.

    El RecyclerView 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
  1. Dentro del SleepNightAdapter, crea una función para anular getItemViewType() 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

  1. En la definición de SleepNightAdapter, actualiza el primer argumento de ListAdapter de SleepNight a DataItem.
  2. En la definición de SleepNightAdapter, cambia el segundo argumento genérico para ListAdapter de SleepNightAdapter.ViewHolder a RecyclerView.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()

  1. Cambia la firma de onCreateViewHolder() para mostrar un RecyclerView.ViewHolder.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
  1. 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()

  1. Cambia el tipo de parámetro de onBindViewHolder() de ViewHolder a RecyclerView.ViewHolder.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
  1. Agrega una condición para asignar solo datos al contenedor de vistas si el contenedor es un ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. Transmite el tipo de objeto que muestra getItem() a DataItem.SleepNightItem. La función onBindViewHolder() 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

  1. Cambia los métodos de SleepNightDiffCallback para usar la clase nueva DataItem en lugar de SleepNight. 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

  1. Dentro de SleepNightAdapter, debajo de onCreateViewHolder(), define una función addHeaderAndSubmitList(), como se muestra a continuación. Esta función toma una lista de SleepNight. En lugar de usar submitList(), que proporciona ListAdapter, 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>?) {}
  1. Dentro de addHeaderAndSubmitList(), si el pase en la lista es null, 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)
  1. Abre SleepTrackerFragment.kt y cambia la llamada a submitList() por addHeaderAndSubmitList().
  1. 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:

  1. En el nivel superior de la clase SleepNightAdapter, define un elemento CoroutineScope con Dispatchers.Default.
private val adapterScope = CoroutineScope(Dispatchers.Default)
  1. En addHeaderAndSubmitList(), inicia una corrutina en el adapterScope para manipular la lista. Luego, cambia al contexto Dispatchers.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)
            }
        }
    }
  1. 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.

  1. Abre SleepTrackerFragment.kt.
  2. Busca el código en el que defines manager hacia el final de onCreateView().
val manager = GridLayoutManager(activity, 3)
  1. Debajo de manager, define manager.spanSizeLookup, como se muestra. Debes realizar una object porque setSpanSizeLookup no toma una expresión lambda. Para hacer una object en Kotlin, escribe object : classname, en este caso GridLayoutManager.SpanSizeLookup.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. Es posible que recibas un error de compilador para llamar al constructor. Si lo haces, abre el menú de intents con Option+Enter (Mac) o Alt+Enter (Windows) para aplicar la llamada al constructor.
  1. Luego, verás un error en object que indica que debes anular los métodos. Coloca el cursor en object, presiona Option+Enter (Mac) o Alt+Enter (Windows) para abrir el menú de intenciones y, luego, anula el método getSpanSize().
  1. 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
            }
        }
  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"
  1. 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 clase DataItem.
  • Crea una función addHeaderAndSubmitList() que use corrutinas para agregar el encabezado al conjunto de datos y, luego, llame a submitList().
  • 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: 8.1 Cómo obtener datos de Internet

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.