Principes de base d'Android en Kotlin 07.5 : En-têtes dans RecyclerView

Cet atelier de programmation fait partie du cours Principes de base d'Android en Kotlin. Vous tirerez pleinement parti de ce cours en suivant les ateliers de programmation dans l'ordre. Tous les ateliers de programmation du cours sont listés sur la page de destination des ateliers de programmation Principes de base d'Android en Kotlin.

Introduction

Dans cet atelier de programmation, vous allez apprendre à ajouter un en-tête qui s'étend sur toute la largeur de la liste affichée dans un RecyclerView. Vous allez vous appuyer sur l'application de suivi du sommeil des ateliers de programmation précédents.

Ce que vous devez déjà savoir

  • Vous savez comment créer une interface utilisateur de base à l'aide d'une activité, de fragments et de vues.
  • Comment naviguer entre les fragments et utiliser safeArgs pour transmettre des données entre les fragments.
  • Affichez les modèles, les fabriques de modèles, les transformations et les LiveData, ainsi que leurs observateurs.
  • Création d'une base de données Room, d'un DAO et définition d'entités
  • Utiliser des coroutines pour les interactions avec la base de données et d'autres tâches de longue durée
  • Implémenter un RecyclerView de base avec une mise en page Adapter, ViewHolder et d'élément.
  • Découvrez comment implémenter la liaison de données pour RecyclerView.
  • Découvrez comment créer et utiliser des adaptateurs de liaison pour transformer des données.
  • Comment utiliser GridLayoutManager
  • Capturer et gérer les clics sur les éléments d'un RecyclerView.

Points abordés

  • Comment utiliser plusieurs ViewHolder avec un RecyclerView pour ajouter des éléments avec une mise en page différente. Plus précisément, comment utiliser un deuxième ViewHolder pour ajouter un en-tête au-dessus des éléments affichés dans RecyclerView.

Objectifs de l'atelier

  • Utilisez l'application TrackMySleepQuality de l'atelier de programmation précédent de cette série.
  • Ajoutez un en-tête qui s'étend sur toute la largeur de l'écran au-dessus des nuits de sommeil affichées dans RecyclerView.

L'application de suivi du sommeil avec laquelle vous commencez comporte trois écrans, représentés par des fragments, comme illustré dans la figure ci-dessous.

Le premier écran, affiché à gauche, comporte des boutons permettant de démarrer et d'arrêter le suivi. L'écran affiche certaines données de sommeil de l'utilisateur. Le bouton Effacer supprime définitivement toutes les données que l'application a collectées pour l'utilisateur. Le deuxième écran, au milieu, permet de sélectionner une note de qualité du sommeil. Le troisième écran est une vue détaillée qui s'ouvre lorsque l'utilisateur appuie sur un élément de la grille.

Cette application utilise une architecture simplifiée avec un contrôleur d'UI, un ViewModel et LiveData, ainsi qu'une base de données Room pour conserver les données de sommeil.

Dans cet atelier de programmation, vous allez ajouter un en-tête à la grille d'éléments affichée. Votre écran principal final se présentera comme suit :

Cet atelier de programmation vous apprend le principe général d'inclusion d'éléments utilisant différentes mises en page dans un RecyclerView. Un exemple courant consiste à inclure des en-têtes dans votre liste ou votre grille. Une liste peut comporter un seul en-tête pour décrire le contenu des éléments. Une liste peut également comporter plusieurs en-têtes pour regrouper et séparer les éléments d'une même liste.

RecyclerView ne connaît rien de vos données ni du type de mise en page de chaque élément. LayoutManager organise les éléments à l'écran, mais l'adaptateur adapte les données à afficher et transmet les porte-vues à RecyclerView. Vous allez donc ajouter le code pour créer des en-têtes dans l'adaptateur.

Deux façons d'ajouter des en-têtes

Dans RecyclerView, chaque élément de la liste correspond à un numéro d'index commençant par 0. Exemple :

[Actual Data] -> [Adapter Views]

[0: SleepNight] -> [0: SleepNight]

[1: SleepNight] -> [1: SleepNight]

[2: SleepNight] -> [2: SleepNight]

Pour ajouter des en-têtes à une liste, vous pouvez modifier votre adaptateur afin d'utiliser un ViewHolder différent en vérifiant les index où votre en-tête doit s'afficher. Le Adapter sera chargé de suivre l'en-tête. Par exemple, pour afficher un en-tête en haut du tableau, vous devez renvoyer un ViewHolder différent pour l'en-tête tout en disposant l'élément à l'index 0. Tous les autres éléments seraient ensuite mis en correspondance avec le décalage de l'en-tête, comme indiqué ci-dessous.

[Actual Data] -> [Adapter Views]

[0: Header]

[0: SleepNight] -> [1: SleepNight]

[1: SleepNight] -> [2: SleepNight]

[2: SleepNight] -> [3: SleepNight.

Une autre façon d'ajouter des en-têtes consiste à modifier l'ensemble de données sous-jacent de votre grille de données. Étant donné que toutes les données à afficher sont stockées dans une liste, vous pouvez modifier cette liste pour inclure des éléments représentant un en-tête. Cette approche est un peu plus simple à comprendre, mais elle vous oblige à réfléchir à la manière dont vous concevez vos objets afin de pouvoir combiner les différents types d'éléments dans une seule liste. Ainsi implémenté, l'adaptateur affichera les éléments qui lui sont transmis. L'élément à la position 0 est donc un en-tête, et celui à la position 1 est un SleepNight, qui correspond directement à ce qui s'affiche à l'écran.

[Actual Data] -> [Adapter Views]

[0: Header] -> [0: Header]

[1: SleepNight] -> [1: SleepNight]

[2: SleepNight] -> [2: SleepNight]

[3: SleepNight] -> [3: SleepNight]

Chaque méthodologie présente des avantages et des inconvénients. La modification de l'ensemble de données n'entraîne pas beaucoup de changements dans le reste du code de l'adaptateur. Vous pouvez ajouter une logique d'en-tête en manipulant la liste de données. En revanche, l'utilisation d'un ViewHolder différent en cochant les index des en-têtes offre plus de liberté sur la mise en page de l'en-tête. Il permet également à l'adaptateur de gérer la façon dont les données sont adaptées à la vue sans modifier les données sous-jacentes.

Dans cet atelier de programmation, vous allez mettre à jour votre RecyclerView pour afficher un en-tête au début de la liste. Dans ce cas, votre application utilisera un ViewHolder différent pour l'en-tête et pour les éléments de données. L'application vérifie l'index de la liste pour déterminer quel ViewHolder utiliser.

Étape 1 : Créer une classe DataItem

Pour abstraire le type d'élément et permettre à l'adaptateur de ne traiter que les "éléments", vous pouvez créer une classe de support de données qui représente un SleepNight ou un Header. Votre ensemble de données sera alors une liste d'éléments de détenteur de données.

Vous pouvez obtenir l'application de démarrage depuis GitHub ou continuer à utiliser l'application SleepTracker que vous avez créée dans l'atelier de programmation précédent.

  1. Téléchargez le code RecyclerViewHeaders-Starter depuis GitHub. Le répertoire RecyclerViewHeaders-Starter contient la version de démarrage de l'application SleepTracker nécessaire pour cet atelier de programmation. Si vous le souhaitez, vous pouvez également continuer avec l'application que vous avez terminée lors de l'atelier de programmation précédent.
  2. Ouvrez SleepNightAdapter.kt.
  3. Sous la classe SleepNightListener, au niveau supérieur, définissez une classe sealed nommée DataItem qui représente un élément de données.

    Une classe sealed définit un type fermé, ce qui signifie que toutes les sous-classes de DataItem doivent être définies dans ce fichier. Par conséquent, le compilateur connaît le nombre de sous-classes. Il n'est pas possible qu'une autre partie de votre code définisse un nouveau type de DataItem qui pourrait casser votre adaptateur.
sealed class DataItem {

 }
  1. Dans le corps de la classe DataItem, définissez deux classes qui représentent les différents types d'éléments de données. La première est une SleepNightItem, qui est un wrapper autour d'un SleepNight. Elle prend donc une seule valeur appelée sleepNight. Pour l'intégrer à la classe scellée, faites-la étendre DataItem.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
  1. La deuxième classe est Header, qui représente un en-tête. Étant donné qu'un en-tête ne contient aucune donnée réelle, vous pouvez le déclarer comme object. Cela signifie qu'il n'y aura jamais qu'une seule instance de Header. Une fois encore, faites-le étendre DataItem.
object Header: DataItem()
  1. Dans DataItem, au niveau de la classe, définissez une propriété abstract Long nommée id. Lorsque l'adaptateur utilise DiffUtil pour déterminer si et comment un élément a changé, DiffItemCallback doit connaître l'ID de chaque élément. Une erreur s'affiche, car SleepNightItem et Header doivent remplacer la propriété abstraite id.
abstract val id: Long
  1. Dans SleepNightItem, remplacez id pour renvoyer nightId.
override val id = sleepNight.nightId
  1. Dans Header, remplacez id pour renvoyer Long.MIN_VALUE, qui est un très, très petit nombre (littéralement, -2 à la puissance 63). Par conséquent, il n'entrera jamais en conflit avec un nightId existant.
override val id = Long.MIN_VALUE
  1. Votre code, une fois fini, doit ressembler à ceci, et votre application doit se compiler sans erreur.
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
    }
}

Étape 2 : Créez un ViewHolder pour l'en-tête

  1. Créez la mise en page de l'en-tête dans un fichier de ressources de mise en page nommé header.xml qui affiche un TextView. Il n'y a rien d'exceptionnel à ce sujet, voici donc le code.
<?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. Extrayez "Sleep Results" dans une ressource de chaîne et appelez-la header_text.
<string name="header_text">Sleep Results</string>
  1. Dans SleepNightAdapter.kt, à l'intérieur de SleepNightAdapter, au-dessus de la classe ViewHolder, créez une classe TextViewHolder. Cette classe augmente la mise en page textview.xml et renvoie une instance TextViewHolder. Comme vous l'avez déjà fait, voici le code. Vous devrez importer View et 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)
            }
        }
    }

Étape 3 : Mettez à jour SleepNightAdapter

Vous devez ensuite mettre à jour la déclaration de SleepNightAdapter. Au lieu de ne prendre en charge qu'un seul type de ViewHolder, il doit pouvoir utiliser n'importe quel type de conteneur de vue.

Définir les types d'articles

  1. Dans SleepNightAdapter.kt, au niveau supérieur, sous les instructions import et au-dessus de SleepNightAdapter, définissez deux constantes pour les types de vues.

    Le RecyclerView devra distinguer le type de vue de chaque élément afin de pouvoir lui attribuer correctement un support de vue.
    private val ITEM_VIEW_TYPE_HEADER = 0
    private val ITEM_VIEW_TYPE_ITEM = 1
  1. Dans SleepNightAdapter, créez une fonction pour remplacer getItemViewType() afin de renvoyer la constante d'en-tête ou d'élément appropriée en fonction du type de l'élément actuel.
override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
            is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
        }
    }

Mettre à jour la définition SleepNightAdapter

  1. Dans la définition de SleepNightAdapter, remplacez le premier argument de ListAdapter par DataItem au lieu de SleepNight.
  2. Dans la définition de SleepNightAdapter, remplacez le deuxième argument générique de ListAdapter par RecyclerView.ViewHolder au lieu de SleepNightAdapter.ViewHolder. Des erreurs s'affichent pour les mises à jour nécessaires. L'en-tête de votre classe doit ressembler à celui ci-dessous.
class SleepNightAdapter(val clickListener: SleepNightListener):
       ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {

Mettre à jour onCreateViewHolder()

  1. Modifiez la signature de onCreateViewHolder() pour renvoyer un RecyclerView.ViewHolder.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
  1. Développez l'implémentation de la méthode onCreateViewHolder() pour tester et renvoyer le support de vue approprié pour chaque type d'élément. Votre méthode mise à jour devrait ressembler au code ci-dessous.
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}")
        }
    }

Mettre à jour onBindViewHolder()

  1. Remplacez le type de paramètre de onBindViewHolder() (ViewHolder) par RecyclerView.ViewHolder.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
  1. Ajoutez une condition pour n'attribuer des données au support de vue que si celui-ci est un ViewHolder.
        when (holder) {
            is ViewHolder -> {...}
  1. Castez le type d'objet renvoyé par getItem() sur DataItem.SleepNightItem. Votre fonction onBindViewHolder() terminée devrait se présenter comme suit.
  override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
            is ViewHolder -> {
                val nightItem = getItem(position) as DataItem.SleepNightItem
                holder.bind(nightItem.sleepNight, clickListener)
            }
        }
    }

Mettre à jour les rappels diffUtil

  1. Modifiez les méthodes dans SleepNightDiffCallback pour qu'elles utilisent votre nouvelle classe DataItem au lieu de SleepNight. Supprimez l'avertissement lint, comme indiqué dans le code ci-dessous.
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
    }
}

Ajouter et envoyer l'en-tête

  1. Dans SleepNightAdapter, sous onCreateViewHolder(), définissez une fonction addHeaderAndSubmitList() comme indiqué ci-dessous. Cette fonction accepte une liste de SleepNight. Au lieu d'utiliser submitList(), fourni par ListAdapter, pour envoyer votre liste, vous utiliserez cette fonction pour ajouter un en-tête, puis envoyer la liste.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
  1. Dans addHeaderAndSubmitList(), si la liste transmise est null, renvoyez uniquement un en-tête. Sinon, ajoutez l'en-tête au début de la liste, puis envoyez la liste.
val items = when (list) {
                null -> listOf(DataItem.Header)
                else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
            }
submitList(items)
  1. Ouvrez SleepTrackerFragment.kt et remplacez l'appel à submitList() par addHeaderAndSubmitList().
  1. Exécutez votre application et observez comment votre en-tête s'affiche comme premier élément de la liste des éléments de sommeil.

Deux problèmes doivent être résolus pour cette application. L'un est visible, l'autre ne l'est pas.

  • L'en-tête s'affiche en haut à gauche et n'est pas facilement identifiable.
  • Cela n'a pas beaucoup d'importance pour une liste courte avec un seul en-tête, mais vous ne devez pas manipuler de liste dans addHeaderAndSubmitList() sur le thread UI. Imaginez une liste contenant des centaines d'éléments, plusieurs en-têtes et une logique permettant de déterminer où les éléments doivent être insérés. Ce travail appartient à une coroutine.

Modifiez addHeaderAndSubmitList() pour utiliser des coroutines :

  1. Au niveau supérieur de la classe SleepNightAdapter, définissez un CoroutineScope avec Dispatchers.Default.
private val adapterScope = CoroutineScope(Dispatchers.Default)
  1. Dans addHeaderAndSubmitList(), lancez une coroutine dans adapterScope pour manipuler la liste. Passez ensuite au contexte Dispatchers.Main pour envoyer la liste, comme indiqué dans le code ci-dessous.
 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. Votre code devrait se compiler et s'exécuter sans que vous ne remarquiez de différence.

Actuellement, l'en-tête a la même largeur que les autres éléments de la grille, occupant une étendue horizontalement et verticalement. L'ensemble de la grille peut contenir trois éléments d'une largeur de portée horizontalement. L'en-tête doit donc utiliser trois portées horizontalement.

Pour corriger la largeur de l'en-tête, vous devez indiquer à GridLayoutManager quand étendre les données sur toutes les colonnes. Pour ce faire, configurez le SpanSizeLookup sur un GridLayoutManager. Il s'agit d'un objet de configuration que GridLayoutManager utilise pour déterminer le nombre de spans à utiliser pour chaque élément de la liste.

  1. Ouvrez SleepTrackerFragment.kt.
  2. Recherchez le code où vous définissez manager, vers la fin de onCreateView().
val manager = GridLayoutManager(activity, 3)
  1. Sous manager, définissez manager.spanSizeLookup, comme indiqué. Vous devez créer un object, car setSpanSizeLookup n'accepte pas de lambda. Pour créer un object en Kotlin, saisissez object : classname, dans ce cas GridLayoutManager.SpanSizeLookup.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
  1. Vous pouvez obtenir une erreur de compilateur pour appeler le constructeur. Si c'est le cas, ouvrez le menu d'intention avec Option+Enter (Mac) ou Alt+Enter (Windows) pour appliquer l'appel du constructeur.
  1. Une erreur s'affichera alors sur object, indiquant que vous devez remplacer des méthodes. Placez le curseur sur object, appuyez sur Option+Enter (Mac) ou Alt+Enter (Windows) pour ouvrir le menu des intentions, puis remplacez la méthode getSpanSize().
  1. Dans le corps de getSpanSize(), renvoyez la bonne taille de span pour chaque position. La position 0 a une taille de portée de 3, et les autres positions ont une taille de portée de 1. Votre code, une fois terminé, doit ressembler à ceci :
    manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
            override fun getSpanSize(position: Int) =  when (position) {
                0 -> 3
                else -> 1
            }
        }
  1. Pour améliorer l'apparence de votre en-tête, ouvrez header.xml et ajoutez ce code au fichier de mise en page 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. Exécutez votre application. Elle devrait ressembler à la capture d'écran ci-dessous.

Félicitations ! Vous avez terminé.

Projet Android Studio : RecyclerViewHeaders

  • Un en-tête est généralement un élément qui s'étend sur toute la largeur d'une liste et sert de titre ou de séparateur. Une liste peut comporter un seul en-tête pour décrire le contenu des éléments ou plusieurs en-têtes pour regrouper les éléments et les séparer les uns des autres.
  • Un RecyclerView peut utiliser plusieurs conteneurs de vue pour s'adapter à un ensemble hétérogène d'éléments (par exemple, des en-têtes et des éléments de liste).
  • Pour ajouter des en-têtes, vous pouvez modifier votre adaptateur afin d'utiliser un autre ViewHolder en vérifiant les index où votre en-tête doit s'afficher. Le Adapter est chargé de suivre l'en-tête.
  • Une autre façon d'ajouter des en-têtes consiste à modifier l'ensemble de données sous-jacent (la liste) de votre grille de données, comme vous l'avez fait dans cet atelier de programmation.

Voici les principales étapes à suivre pour ajouter un en-tête :

  • Abstrayez les données de votre liste en créant un DataItem pouvant contenir un en-tête ou des données.
  • Créez un conteneur de vue avec une mise en page pour l'en-tête dans l'adaptateur.
  • Mettez à jour l'adaptateur et ses méthodes pour utiliser n'importe quel type de RecyclerView.ViewHolder.
  • Dans onCreateViewHolder(), renvoyez le type correct de support de vue pour l'élément de données.
  • Mettez à jour SleepNightDiffCallback pour qu'il fonctionne avec la classe DataItem.
  • Créez une fonction addHeaderAndSubmitList() qui utilise des coroutines pour ajouter l'en-tête à l'ensemble de données, puis appelle submitList().
  • Implémentez GridLayoutManager.SpanSizeLookup() pour que seul l'en-tête s'étende sur trois colonnes.

Cours Udacity :

Documentation pour les développeurs Android :

Cette section répertorie les devoirs possibles pour les élèves qui suivent cet atelier de programmation dans le cadre d'un cours animé par un enseignant. Il revient à l'enseignant d'effectuer les opérations suivantes :

  • Attribuer des devoirs si nécessaire
  • Indiquer aux élèves comment rendre leurs devoirs
  • Noter les devoirs

Les enseignants peuvent utiliser ces suggestions autant qu'ils le souhaitent, et ne doivent pas hésiter à attribuer d'autres devoirs aux élèves s'ils le jugent nécessaire.

Si vous suivez cet atelier de programmation par vous-même, n'hésitez pas à utiliser ces devoirs pour tester vos connaissances.

Répondre aux questions suivantes

Question 1

Parmi les affirmations suivantes concernant ViewHolder, laquelle est vraie ?

▢ Un adaptateur peut utiliser plusieurs classes ViewHolder pour contenir des en-têtes et divers types de données.

▢ Vous pouvez disposer d'un conteneur de vue pour les données et d'un autre pour un en-tête.

▢ Un RecyclerView accepte plusieurs types d'en-têtes, mais les données doivent être uniformisées.

▢ Lorsque vous ajoutez un en-tête, vous sous-classez RecyclerView pour insérer l'en-tête à la bonne position.

Question 2

Quand utiliser des coroutines avec un RecyclerView ? Sélectionnez toutes les affirmations vraies.

▢ Jamais Un RecyclerView est un élément d'UI et ne doit pas utiliser de coroutines.

▢ Utilisez des coroutines pour les tâches de longue durée qui pourraient ralentir l'UI.

▢ Les manipulations de listes peuvent prendre beaucoup de temps. Vous devez toujours les effectuer à l'aide de coroutines.

▢ Utilisez des coroutines avec des fonctions de suspension pour éviter de bloquer le thread principal.

Question 3

Parmi les actions suivantes, laquelle n'est PAS requise lorsque vous utilisez plusieurs ViewHolder ?

▢ Dans ViewHolder, fournissez plusieurs fichiers de mise en page à développer selon vos besoins.

▢ Dans onCreateViewHolder(), renvoyez le type correct de support de vue pour l'élément de données.

▢ Dans onBindViewHolder(), n'associez les données que si le support de vue est le bon type de support de vue pour l'élément de données.

▢ Généralisez la signature de la classe d'adaptateur pour accepter n'importe quel RecyclerView.ViewHolder.

Passer à la leçon suivante : 8.1 Récupérer des données sur Internet

Pour obtenir des liens vers d'autres ateliers de programmation de ce cours, consultez la page de destination des ateliers de programmation Principes de base d'Android en Kotlin.