Aspectos básicos de Kotlin para Android 07.1: Aspectos básicos de 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 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 usar un RecyclerView para mostrar listas de elementos. A partir de la app de Sleep Tracker de la serie anterior de codelabs, aprenderás una forma mejor y más versátil de mostrar datos con un RecyclerView y una arquitectura recomendada.

Conocimientos que ya deberías tener

Debes estar familiarizado con lo siguiente:

  • Compilar una interfaz de usuario (IU) básica con una actividad, fragmentos y vistas
  • Navegar entre fragmentos y usar safeArgs para pasar datos entre fragmentos
  • Cómo usar modelos de vistas, fábricas de modelos de vistas, transformaciones y LiveData y sus observadores
  • Crear una base de datos de Room, crear un DAO y definir entidades
  • Usar corrutinas para tareas de bases de datos y otras tareas de larga duración

Qué aprenderás

  • Cómo usar un RecyclerView con un Adapter y un ViewHolder para mostrar una lista de elementos

Actividades

  • Cambia la app de TrackMySleepQuality de la lección anterior para usar un RecyclerView y mostrar los datos de calidad del sueño.

En este codelab, compilarás la parte RecyclerView de una app que hace un seguimiento de la calidad del sueño. La app usa una base de datos Room para almacenar los datos de sueño a lo largo del tiempo.

La app de inicio de seguimiento del sueño 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 esta pantalla, también se muestran todos 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, ViewModel y LiveData. La app también usa una base de datos Room para que los datos de sueño sean persistentes.

La lista de noches de sueño que se muestra en la primera pantalla es funcional, pero no atractiva. La app usa un formateador complejo para crear cadenas de texto para la vista de texto y números para la calidad. Además, este diseño no se adapta a diferentes escalas. Después de solucionar todos estos problemas en este codelab, la app final tendrá la misma funcionalidad y la pantalla principal se verá de la siguiente manera:

Mostrar una lista o cuadrícula de datos es una de las tareas de IU más comunes en Android. Las listas varían de simples a muy complejas. Una lista de vistas de texto puede mostrar datos simples, como una lista de compras. Una lista compleja, como una lista anotada de destinos de vacaciones, puede mostrarle al usuario muchos detalles dentro de una cuadrícula desplazable con encabezados.

Para admitir todos estos casos de uso, Android proporciona el widget RecyclerView.

El mayor beneficio de RecyclerView es que es muy eficiente para listas grandes:

  • De forma predeterminada, RecyclerView solo trabaja para procesar o dibujar elementos que estén visibles actualmente en la pantalla. Por ejemplo, si tu lista tiene mil elementos, pero solo 10 son visibles, RecyclerView solo hace el trabajo correspondiente para dibujar 10 elementos en la pantalla. Cuando el usuario se desplaza, RecyclerView detecta los elementos nuevos que deben aparecer en la pantalla y hace el trabajo necesario para mostrar solo esos elementos.
  • Cuando un elemento se desplaza fuera de la pantalla, se reciclan sus vistas. Esto significa que el elemento se rellena con contenido nuevo que se desplaza hacia la pantalla. Este comportamiento de RecyclerView ahorra mucho tiempo de procesamiento y ayuda a las listas a desplazarse de forma fluida.
  • Cuando cambia un elemento, en lugar de volver a dibujar toda la lista, RecyclerView puede actualizar ese elemento. Esto representa una gran mejora en la eficiencia cuando se muestran listas de elementos complejos.

En la secuencia que se muestra a continuación, puedes ver que se completó una vista con datos, ABC. Después de que la vista se desplace fuera de la pantalla, RecyclerView volverá a usar la vista para datos nuevos, XYZ.

El patrón de adaptador

Si alguna vez viajaste entre países que usan enchufes eléctricos diferentes, probablemente sepas cómo conectar tus dispositivos a los tomacorrientes con un adaptador. El adaptador te permite convertir un tipo de enchufe en otro, lo que en realidad convierte una interfaz en otra.

El patrón de adaptador en la ingeniería de software ayuda a un objeto a trabajar con otra API. RecyclerView usa un adaptador para transformar los datos de la app en algo que RecyclerView pueda mostrar, sin cambiar la forma en que la app almacena y procesa los datos. En el caso de la app de monitoreo del sueño, compilarás un adaptador que adapte los datos de la base de datos Room a algo que RecyclerView sepa cómo mostrar, sin cambiar ViewModel.

Cómo implementar un RecyclerView

Para mostrar tus datos en un RecyclerView, necesitas las siguientes partes:

  • Datos que se mostrarán.
  • Una instancia de RecyclerView definida en tu archivo de diseño para que actúe como contenedor de las vistas
  • Diseño para un elemento de datos.
    Si todos los elementos de la lista se ven iguales, puedes usar el mismo diseño para todos, pero no es obligatorio. El diseño del elemento se debe crear por separado del diseño del fragmento, de modo que se pueda crear y completar con datos una vista de elemento a la vez.
  • Es un administrador de diseño.
    El administrador de diseño controla la organización (el diseño) de los componentes de la IU en una vista.
  • Un contenedor de vistas.
    El contenedor de vistas extiende la clase ViewHolder. Contiene la información de la vista para mostrar un elemento del diseño del elemento. Los contenedores de vistas también agregan información que RecyclerView usa para mover las vistas de manera eficiente por la pantalla.
  • Un adaptador.
    El adaptador conecta tus datos a RecyclerView. Adapta los datos para que se puedan mostrar en un ViewHolder. Un RecyclerView usa el adaptador para descifrar cómo mostrar los datos en la pantalla.

En esta tarea, agregarás un RecyclerView a tu archivo de diseño y configurarás un Adapter para exponer los datos de sueño al RecyclerView.

Paso 1: Agrega RecyclerView con LayoutManager

En este paso, reemplazarás el ScrollView por un RecyclerView en el archivo fragment_sleep_tracker.xml.

  1. Descarga la app RecyclerViewFundamentals-Starter desde GitHub.
  2. Compila y ejecuta la app. Observa cómo se muestran los datos como texto simple.
  3. Abre el archivo de diseño fragment_sleep_tracker.xml en la pestaña Design de Android Studio.
  4. En el panel Component Tree, borra el elemento ScrollView. Esta acción también borra el TextView que se encuentra dentro del ScrollView.
  5. En el panel Palette, desplázate por la lista de tipos de componentes que se encuentra a la izquierda para encontrar Containers y, luego, selecciónalo.
  6. Arrastra un RecyclerView del panel Palette al panel Component Tree. Coloca el elemento RecyclerView dentro del elemento ConstraintLayout.

  1. Si se abre un diálogo que te pregunta si deseas agregar una dependencia, haz clic en OK para permitir que Android Studio agregue la dependencia recyclerview a tu archivo Gradle. Es posible que tarde unos segundos y, luego, se sincronizará tu app.

  1. Abre el archivo build.gradle del módulo, desplázate hasta el final y toma nota de la nueva dependencia, que es similar al siguiente código:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. Vuelve a cambiar a fragment_sleep_tracker.xml.
  2. En la pestaña Text, busca el código RecyclerView que se muestra a continuación:
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. Asigna al RecyclerView un id de sleep_list.
android:id="@+id/sleep_list"
  1. Coloca el RecyclerView de modo que ocupe la parte restante de la pantalla dentro del ConstraintLayout. Para ello, restringe la parte superior del RecyclerView al botón Start, la parte inferior al botón Clear y cada lado al elemento superior. Establece el ancho y la altura del diseño en 0 dp en el editor de diseño o en XML con el siguiente código:
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@+id/clear_button"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/stop_button"
  1. Agrega un administrador de diseño al XML de RecyclerView. Cada RecyclerView necesita un administrador de diseño que le indique cómo posicionar los elementos en la lista. Android proporciona un LinearLayoutManager que, de forma predeterminada, organiza los elementos en una lista vertical de filas de ancho completo.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. Cambia a la pestaña Design y observa que las restricciones agregadas hicieron que RecyclerView se expandiera para ocupar el espacio disponible.

Paso 2: Crea el diseño del elemento de lista y el contenedor de TextView

El RecyclerView es solo un contenedor. En este paso, crearás el diseño y la infraestructura para que los elementos se muestren dentro de RecyclerView.

Para obtener un RecyclerView funcional lo más rápido posible, al principio usarás un elemento de lista simplista que solo muestre la calidad del sueño como un número. Para ello, necesitas un elemento de ViewHolder, TextItemViewHolder. También necesitas una vista, un TextView, para los datos. (En un paso posterior, aprenderás más sobre los titulares de vistas y cómo diseñar todos los datos de sueño).

  1. Crea un archivo de diseño llamado text_item_view.xml. No importa qué uses como elemento raíz, ya que reemplazarás el código de la plantilla.
  2. En text_item_view.xml, borra todo el código proporcionado.
  3. Agrega un TextView con un padding de 16dp al principio y al final, y un tamaño de texto de 24sp. Permite que el ancho coincida con el elemento superior y que la altura ajuste el contenido. Como esta vista se muestra dentro de RecyclerView, no es necesario que la coloques dentro de un ViewGroup.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:textSize="24sp"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:layout_width="match_parent"       
    android:layout_height="wrap_content" />
  1. Abre Util.kt. Desplázate hasta el final y agrega la definición que se muestra a continuación, que crea la clase TextItemViewHolder. Coloca el código en la parte inferior del archivo, después de la última llave de cierre. El código va en Util.kt porque este elemento de diseño de la vista es temporal y lo reemplazarás más adelante.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. Si se te solicita, importa android.widget.TextView y androidx.recyclerview.widget.RecyclerView.

Paso 3: Crea SleepNightAdapter

La tarea principal para implementar un RecyclerView es crear el adaptador. Tienes un contenedor de vistas simple para la vista del elemento y un diseño para cada elemento. Ahora puedes crear un adaptador. El adaptador crea un contenedor de vistas y lo completa con datos para que se muestre el RecyclerView.

  1. En el paquete sleeptracker, crea una nueva clase de Kotlin llamada SleepNightAdapter.
  2. Haz que la clase SleepNightAdapter extienda RecyclerView.Adapter. La clase se llama SleepNightAdapter porque adapta un objeto SleepNight en algo que RecyclerView puede usar. El adaptador necesita saber qué contenedor de vistas usar, por lo que debes pasar TextItemViewHolder. Importa los componentes necesarios cuando se te solicite y, luego, verás un error porque hay métodos obligatorios para implementar.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. En el nivel superior de SleepNightAdapter, crea una variable listOf SleepNight para contener los datos.
var data =  listOf<SleepNight>()
  1. En SleepNightAdapter, anula getItemCount() para devolver el tamaño de la lista de noches de sueño en data. El RecyclerView necesita saber cuántos elementos tiene el adaptador para mostrar, y lo hace llamando a getItemCount().
override fun getItemCount() = data.size
  1. En SleepNightAdapter, anula la función onBindViewHolder(), como se muestra a continuación.

    RecyclerView llama a la función onBindViewHolder() para mostrar los datos de un elemento de la lista en la posición especificada. Por lo tanto, el método onBindViewHolder() toma dos argumentos: un contenedor de vistas y una posición de los datos para vincular. En esta app, el titular es TextItemViewHolder y la posición es la posición en la lista.
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
  1. Dentro de onBindViewHolder(), crea una variable para un elemento en una posición determinada en los datos.
 val item = data[position]
  1. El ViewHolder que creaste tiene una propiedad llamada textView. Dentro de onBindViewHolder(), establece el text de textView en el número de calidad del sueño. Este código solo muestra una lista de números, pero este ejemplo simple te permite ver cómo el adaptador lleva los datos al ViewHolder y a la pantalla.
holder.textView.text = item.sleepQuality.toString()
  1. En SleepNightAdapter, anula e implementa onCreateViewHolder(), al que se llama cuando RecyclerView necesita un contenedor de vistas para representar un elemento.

    Esta función toma dos parámetros y devuelve un ViewHolder. El parámetro parent, que es el grupo de vistas que contiene el elemento de diseño de la vista, siempre es RecyclerView. El parámetro viewType se usa cuando hay varias vistas en el mismo RecyclerView. Por ejemplo, si colocas una lista de vistas de texto, una imagen y un video en el mismo RecyclerView, la función onCreateViewHolder() debería saber qué tipo de vista usar.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
  1. En onCreateViewHolder(), crea una instancia de LayoutInflater.

    El inflador de diseño sabe cómo crear vistas a partir de diseños en XML. El campo context contiene información sobre cómo inflar correctamente la vista. En un adaptador para una vista de reciclador, siempre pasas el contexto del grupo de vistas parent, que es el RecyclerView.
val layoutInflater = LayoutInflater.from(parent.context)
  1. En onCreateViewHolder(), crea el view pidiéndole al layoutinflater que lo expanda.

    Pasa el diseño XML para la vista y el grupo de vistas parent para la vista. El tercer argumento booleano es attachToRoot. Este argumento debe ser false, porque RecyclerView agrega este elemento a la jerarquía de vistas cuando sea el momento.
val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. En onCreateViewHolder(), devuelve un TextItemViewHolder creado con view.
return TextItemViewHolder(view)
  1. El adaptador debe informar al RecyclerView cuando el data cambió, ya que el RecyclerView no sabe nada sobre los datos. Solo conoce los titulares de vistas que le proporciona el adaptador.

    Para indicarle al RecyclerView cuándo cambiaron los datos que muestra, agrega un setter personalizado a la variable data que se encuentra en la parte superior de la clase SleepNightAdapter. En el setter, asigna un valor nuevo a data y, luego, llama a notifyDataSetChanged() para activar el redibujo de la lista con los datos nuevos.
var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

Paso 4: Informa a RecyclerView sobre el adaptador

El RecyclerView necesita saber qué adaptador usar para obtener contenedores de vistas.

  1. Abre SleepTrackerFragment.kt.
  2. En onCreateview(), crea un adaptador. Coloca este código después de la creación del modelo ViewModel y antes de la instrucción return.
val adapter = SleepNightAdapter()
  1. Asocia el adapter con el RecyclerView.
binding.sleepList.adapter = adapter
  1. Limpia y vuelve a compilar tu proyecto para actualizar el objeto binding.

    Si sigues viendo errores relacionados con binding.sleepList o binding.FragmentSleepTrackerBinding, invalida las memorias caché y reinicia el sistema. (Selecciona File > Invalidate Caches / Restart).

    Si ejecutas la app ahora, no habrá errores, pero no verás ningún dato cuando presiones Start y, luego, Stop.

Paso 5: Obtén datos en el adaptador

Hasta ahora, tienes un adaptador y una forma de obtener datos del adaptador en el RecyclerView. Ahora, debes obtener datos en el adaptador desde ViewModel.

  1. Abre SleepTrackerViewModel.
  2. Busca la variable nights, que almacena todas las noches de sueño, que son los datos que se mostrarán. La variable nights se establece llamando a getAllNights() en la base de datos.
  3. Quita private de nights, ya que crearás un observador que necesita acceder a esta variable. Tu declaración debería verse de la siguiente manera:
val nights = database.getAllNights()
  1. En el paquete database, abre SleepDatabaseDao.
  2. Busca la función getAllNights(). Ten en cuenta que esta función devuelve una lista de valores SleepNight como LiveData. Esto significa que la variable nights contiene LiveData, que Room mantiene actualizada, y puedes observar nights para saber cuándo cambia.
  3. Abre SleepTrackerFragment.
  4. En onCreateView(), debajo de la creación de adapter, crea un observador en la variable nights.

    Si proporcionas el viewLifecycleOwner del fragmento como propietario del ciclo de vida, puedes asegurarte de que este observador solo esté activo cuando el RecyclerView esté en la pantalla.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. Dentro del observador, cada vez que obtengas un valor no nulo (para nights), asígnale el valor a data del adaptador. Este es el código completado para el observador y la configuración de los datos:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. Compila y ejecuta tu código.

    Si el adaptador funciona, verás los números de calidad del sueño como una lista. En la captura de pantalla de la izquierda, se muestra -1 después de que presionas Start. En la captura de pantalla de la derecha, se muestra el número actualizado de la calidad del sueño después de que presionas Detener y seleccionas una calificación de calidad.

Paso 6: Explora cómo se reciclan los titulares de vistas

RecyclerView recicla los contenedores de vistas, lo que significa que los reutiliza. A medida que una vista se desplaza fuera de la pantalla, RecyclerView reutiliza la vista para la vista que está a punto de desplazarse en la pantalla.

Como estos contenedores de vistas se reciclan, asegúrate de que onBindViewHolder() establezca o restablezca cualquier personalización que los elementos anteriores hayan establecido en un contenedor de vistas.

Por ejemplo, puedes establecer el color del texto en rojo en los titulares de vistas que contienen calificaciones de calidad menores o iguales a 1 y representan un sueño deficiente.

  1. En la clase SleepNightAdapter, agrega el siguiente código al final de onBindViewHolder().
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. Ejecuta la app.
  2. Si agregas algunos datos de baja calidad del sueño, el número se mostrará en rojo.
  3. Agrega calificaciones altas para la calidad del sueño hasta que veas un número rojo alto en la pantalla.

    Como RecyclerView reutiliza los contenedores de vistas, eventualmente reutiliza uno de los contenedores de vistas rojos para una calificación de alta calidad. La calificación alta se muestra de forma errónea en rojo.

  1. Para corregir este problema, agrega una instrucción else para establecer el color en negro si la calidad no es menor o igual a uno.

    Con ambas condiciones explícitas, el titular de la vista usará el color de texto correcto para cada elemento.
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. Ejecuta la app y los números siempre deberían tener el color correcto.

¡Felicitaciones! Ahora tienes un RecyclerView básico completamente funcional.

En esta tarea, reemplazarás el titular de vista simple por uno que pueda mostrar más datos de una noche de sueño.

El ViewHolder simple que agregaste a Util.kt solo une un TextView en un TextItemViewHolder.

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

Entonces, ¿por qué RecyclerView no usa directamente un TextView? Esta línea de código proporciona mucha funcionalidad. Un objeto ViewHolder describe una vista de elemento y los metadatos sobre su lugar dentro del objeto RecyclerView. RecyclerView depende de esta funcionalidad para posicionar correctamente la vista a medida que se desplaza la lista y para hacer cosas interesantes, como animar vistas cuando se agregan o quitan elementos en el Adapter.

Si RecyclerView necesita acceder a las vistas almacenadas en ViewHolder, puede hacerlo con la propiedad itemView del contenedor de vistas. RecyclerView usa itemView cuando vincula un elemento para mostrarlo en la pantalla, cuando dibuja decoraciones alrededor de una vista, como un borde, y para implementar la accesibilidad.

Paso 1: Crea el diseño del elemento

En este paso, crearás el archivo de diseño para un elemento. El diseño consta de un ConstraintLayout con un ImageView para la calidad del sueño, un TextView para la duración del sueño y un TextView para la calidad como texto. Como ya creaste diseños antes, copia y pega el código XML proporcionado.

  1. Crea un nuevo archivo de recursos de diseño y asígnale el nombre list_item_sleep_night.
  2. Reemplaza todo el código del archivo por el siguiente. Luego, familiarízate con el diseño que acabas de crear.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content">

   <ImageView
       android:id="@+id/quality_image"
       android:layout_width="@dimen/icon_size"
       android:layout_height="60dp"
       android:layout_marginStart="16dp"
       android:layout_marginTop="8dp"
       android:layout_marginBottom="8dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent"
       tools:srcCompat="@drawable/ic_sleep_5" />

   <TextView
       android:id="@+id/sleep_length"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginStart="8dp"
       android:layout_marginTop="8dp"
       android:layout_marginEnd="16dp"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toEndOf="@+id/quality_image"
       app:layout_constraintTop_toTopOf="@+id/quality_image"
       tools:text="Wednesday" />

   <TextView
       android:id="@+id/quality_string"
       android:layout_width="0dp"
       android:layout_height="20dp"
       android:layout_marginTop="8dp"
       app:layout_constraintEnd_toEndOf="@+id/sleep_length"
       app:layout_constraintHorizontal_bias="0.0"
       app:layout_constraintStart_toStartOf="@+id/sleep_length"
       app:layout_constraintTop_toBottomOf="@+id/sleep_length"
       tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. Cambia a la pestaña Design en Android Studio. En la vista de diseño, tu diseño se ve como la captura de pantalla de la izquierda a continuación. En la vista de esquema, se ve como la captura de pantalla de la derecha.

Paso 2: Crea ViewHolder

  1. Abre SleepNightAdapter.kt.
  2. Crea una clase dentro de SleepNightAdapter llamada ViewHolder y haz que extienda RecyclerView.ViewHolder.
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. Dentro de ViewHolder, obtén referencias a las vistas. Necesitas una referencia a las vistas que actualizará este ViewHolder. Cada vez que vinculas este ViewHolder, debes acceder a la imagen y a ambas vistas de texto. (Convertirás este código para usar la vinculación de datos más adelante).
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)

Paso 3: Usa el ViewHolder en SleepNightAdapter

  1. En la definición de SleepNightAdapter, en lugar de TextItemViewHolder, usa el SleepNightAdapter.ViewHolder que acabas de crear.
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

Actualiza onCreateViewHolder() con este comando:

  1. Cambia la firma de onCreateViewHolder() para que muestre ViewHolder.
  2. Cambia el inflador de diseño para que use el recurso de diseño correcto, list_item_sleep_night.
  3. Quita la transmisión a TextView.
  4. En lugar de devolver un TextItemViewHolder, devuelve un ViewHolder.

    Esta es la función onCreateViewHolder() actualizada finalizada:
    override fun onCreateViewHolder(
            parent: ViewGroup, viewType: Int): ViewHolder {
        val layoutInflater = 
            LayoutInflater.from(parent.context)
        val view = layoutInflater
                .inflate(R.layout.list_item_sleep_night, 
                         parent, false)
        return ViewHolder(view)
    }

Actualiza onBindViewHolder() con este comando:

  1. Cambia la firma de onBindViewHolder() para que el parámetro holder sea un ViewHolder en lugar de un TextItemViewHolder.
  2. Dentro de onBindViewHolder(), borra todo el código, excepto la definición de item.
  3. Define un val res que contenga una referencia al resources de esta vista.
val res = holder.itemView.context.resources
  1. Establece el texto de la vista de texto sleepLength en la duración. Copia el siguiente código, que llama a una función de formato que se proporciona con el código de partida.
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
  1. Esto genera un error porque se debe definir convertDurationToFormatted(). Abre Util.kt y quita el comentario del código y las importaciones asociadas. (Selecciona Code > Comment with Line comments).
  2. De vuelta en onBindViewHolder(), usa convertNumericQualityToString() para establecer la calidad.
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. Es posible que debas importar estas funciones de forma manual.
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. Establece el ícono correcto para la calidad. El nuevo ícono ic_sleep_active se proporciona en el código de partida.
holder.qualityImage.setImageResource(when (item.sleepQuality) {
   0 -> R.drawable.ic_sleep_0
   1 -> R.drawable.ic_sleep_1
   2 -> R.drawable.ic_sleep_2
   3 -> R.drawable.ic_sleep_3
   4 -> R.drawable.ic_sleep_4
   5 -> R.drawable.ic_sleep_5
   else -> R.drawable.ic_sleep_active
})
  1. Esta es la función onBindViewHolder() actualizada finalizada, que establece todos los datos para ViewHolder:
   override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = data[position]
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. Ejecuta la app. La pantalla debería verse como la siguiente captura, en la que se muestra el ícono de calidad del sueño junto con el texto de la duración y la calidad del sueño.

¡Se completó tu RecyclerView! Aprendiste a implementar un Adapter y un ViewHolder, y los combinaste para mostrar una lista con un RecyclerView Adapter.

Hasta ahora, tu código muestra el proceso de creación de un adaptador y un contenedor de vistas. Sin embargo, puedes mejorar este código. El código para mostrar y el código para administrar los titulares de vistas están mezclados, y onBindViewHolder() conoce los detalles sobre cómo actualizar el ViewHolder.

En una app de producción, es posible que tengas varios titulares de vistas, adaptadores más complejos y varios desarrolladores que realizan cambios. Debes estructurar tu código de manera que todo lo relacionado con un contenedor de vistas se encuentre solo en el contenedor de vistas.

Paso 1: Refactoriza onBindViewHolder()

En este paso, refactorizarás el código y moverás toda la funcionalidad del titular de la vista a ViewHolder. El objetivo de esta refactorización no es cambiar la apariencia de la app para el usuario, sino facilitar y hacer más seguro el trabajo de los desarrolladores en el código. Afortunadamente, Android Studio tiene herramientas para ayudarte.

  1. En SleepNightAdapter, en onBindViewHolder(), selecciona todo, excepto la sentencia para declarar la variable item.
  2. Haz clic con el botón derecho y, luego, selecciona Refactor > Extract > Function.
  3. Nombra la función bind y acepta los parámetros sugeridos. Haz clic en OK.

    La función bind() se coloca debajo de onBindViewHolder().
    private fun bind(holder: ViewHolder, item: SleepNight) {
        val res = holder.itemView.context.resources
        holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
        holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
        holder.qualityImage.setImageResource(when (item.sleepQuality) {
            0 -> R.drawable.ic_sleep_0
            1 -> R.drawable.ic_sleep_1
            2 -> R.drawable.ic_sleep_2
            3 -> R.drawable.ic_sleep_3
            4 -> R.drawable.ic_sleep_4
            5 -> R.drawable.ic_sleep_5
            else -> R.drawable.ic_sleep_active
        })
    }
  1. Coloca el cursor sobre la palabra holder del parámetro holder de bind(). Presiona Alt+Enter (Option+Enter en Mac) para abrir el menú de intención. Selecciona Convert parameter to receiver para convertirlo en una función de extensión que tenga la siguiente firma:
private fun ViewHolder.bind(item: SleepNight) {...}
  1. Corta y pega la función bind() en el archivo ViewHolder.
  2. Haz que bind() sea público.
  3. Si es necesario, importa bind() al adaptador.
  4. Como ahora está en ViewHolder, puedes quitar la parte de ViewHolder de la firma. Este es el código final de la función bind() en la clase ViewHolder.
fun bind(item: SleepNight) {
   val res = itemView.context.resources
   sleepLength.text = convertDurationToFormatted(
           item.startTimeMilli, item.endTimeMilli, res)
   quality.text = convertNumericQualityToString(
           item.sleepQuality, res)
   qualityImage.setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

Paso 2: Refactoriza onCreateViewHolder

Actualmente, el método onCreateViewHolder() del adaptador aumenta la vista desde el recurso de diseño para el ViewHolder. Sin embargo, la inflación no tiene nada que ver con el adaptador, sino con el ViewHolder. La inflación debería ocurrir en el ViewHolder.

  1. En onCreateViewHolder(), selecciona todo el código del cuerpo de la función.
  2. Haz clic con el botón derecho y, luego, selecciona Refactor > Extract > Function.
  3. Nombra la función from y acepta los parámetros sugeridos. Haz clic en OK.
  4. Coloca el cursor sobre el nombre de la función from. Presiona Alt+Enter (Option+Enter en Mac) para abrir el menú de intención.
  5. Selecciona Mover al objeto complementario. La función from() debe estar en un objeto complementario para que se pueda llamar en la clase ViewHolder, no en una instancia de ViewHolder.
  6. Mueve el objeto companion a la clase ViewHolder.
  7. Haz que from() sea público.
  8. En onCreateViewHolder(), cambia la instrucción return para que muestre el resultado de llamar a from() en la clase ViewHolder.

    Tus métodos onCreateViewHolder() y from() completados deberían ser similares al siguiente código, y tu código debería compilarse y ejecutarse sin errores.
    override fun onCreateViewHolder(parent: ViewGroup, viewType: 
Int): ViewHolder {
        return ViewHolder.from(parent)
    }
companion object {
   fun from(parent: ViewGroup): ViewHolder {
       val layoutInflater = LayoutInflater.from(parent.context)
       val view = layoutInflater
               .inflate(R.layout.list_item_sleep_night, parent, false)
       return ViewHolder(view)
   }
}
  1. Cambia la firma de la clase ViewHolder para que el constructor sea privado. Dado que from() ahora es un método que devuelve una nueva instancia de ViewHolder, ya no hay motivos para que nadie llame al constructor de ViewHolder.
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. Ejecuta la app. Debería compilarse y ejecutarse de la misma manera que antes, que es el resultado deseado después de la refactorización.

Proyecto de Android Studio: RecyclerViewFundamentals

  • Mostrar una lista o cuadrícula de datos es una de las tareas de IU más comunes en Android. RecyclerView está diseñado para ser eficiente incluso cuando se muestran listas extremadamente grandes.
  • RecyclerView solo realiza el trabajo necesario para procesar o dibujar elementos que estén visibles actualmente en la pantalla.
  • Cuando un elemento se desplaza fuera de la pantalla, se reciclan sus vistas. Esto significa que el elemento se rellena con contenido nuevo que se desplaza hacia la pantalla.
  • El patrón de adaptador en la ingeniería de software ayuda a que un objeto funcione junto con otra API. RecyclerView usa un adaptador para transformar los datos de la app en algo que pueda mostrar, sin necesidad de cambiar la forma en que la app almacena y procesa los datos.

Para mostrar tus datos en un RecyclerView, necesitas las siguientes partes:

  • RecyclerView
    Para crear una instancia de RecyclerView, define un elemento <RecyclerView> en el archivo de diseño.
  • LayoutManager
    Un RecyclerView usa un LayoutManager para organizar el diseño de los elementos en el RecyclerView, por ejemplo, para diseñarlos en una cuadrícula o en una lista lineal.

    En el <RecyclerView> del archivo de diseño, establece el atributo app:layoutManager en el administrador de diseño (como LinearLayoutManager o GridLayoutManager).

    También puedes establecer el LayoutManager para un RecyclerView de forma programática. (Esta técnica se explica en un codelab posterior).
  • Diseño para cada elemento
    Crea un diseño para un elemento de datos en un archivo de diseño XML.
  • Adapter
    Crea un adaptador que prepare los datos y la forma en que se mostrarán en un ViewHolder. Asocia el adaptador con el RecyclerView.

    Cuando se ejecuta RecyclerView, se usa el adaptador para determinar cómo mostrar los datos en la pantalla.

    El adaptador requiere que implementes los siguientes métodos:
    getItemCount() para devolver la cantidad de elementos.
    onCreateViewHolder() para devolver el ViewHolder de un elemento en la lista.
    onBindViewHolder() para adaptar los datos a las vistas de un elemento en la lista.

  • ViewHolder
    Un ViewHolder contiene la información de la vista para mostrar un elemento del diseño del elemento.
  • El método onBindViewHolder() del adaptador adapta los datos a las vistas. Siempre anulas este método. Por lo general, onBindViewHolder() infla el diseño de un elemento y coloca los datos en las vistas del diseño.
  • Dado que RecyclerView no sabe nada sobre los datos, Adapter debe informar a RecyclerView cuando cambien esos datos. Usa notifyDataSetChanged() para notificar al Adapter que los datos cambiaron.

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

¿Cómo muestra elementos RecyclerView? Selecciona todas las opciones que correspondan.

▢ Muestra los elementos en una lista o una cuadrícula.

▢ Se desplaza de forma horizontal o vertical.

▢ Se desplaza de manera diagonal en dispositivos más grandes, como las tablets.

▢ Permite el uso de diseños personalizados cuando una lista o una cuadrícula no alcanzan para el caso de uso determinado.

Pregunta 2

¿Cuáles son los beneficios de usar RecyclerView? Selecciona todas las opciones que correspondan.

▢ Muestra listas extensas de manera eficiente.

▢ Actualiza los datos automáticamente.

▢ Minimiza la necesidad de actualizaciones cuando un elemento se actualiza, se borra o se agrega a la lista.

▢ Reutiliza la vista que se desplaza fuera de la pantalla para mostrar el siguiente elemento que se desplaza en la pantalla.

Pregunta 3

¿Cuáles son algunas de las razones para usar adaptadores? Selecciona todas las opciones que correspondan.

▢ La separación de problemas facilita el cambio y las pruebas del código.

RecyclerView es independiente de los datos que se muestran.

▢ Las capas de procesamiento de datos no necesitan preocuparse por cómo se muestran los datos.

▢ La app se ejecutará más rápido.

Pregunta 4

¿Cuáles de las siguientes afirmaciones sobre ViewHolder son verdaderas? Selecciona todas las opciones que correspondan.

▢ El diseño de ViewHolder se define en los archivos de diseño XML.

▢ Hay un ViewHolder para cada unidad de datos en el conjunto de datos.

▢ Puedes tener más de un ViewHolder en un RecyclerView.

Adapter vincula los datos a ViewHolder.

Comienza la siguiente lección: 7.2: DiffUtil y la vinculación de datos con RecyclerView