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 pageAdapter
,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 unRecyclerView
pour ajouter des éléments avec une mise en page différente. Plus précisément, comment utiliser un deuxièmeViewHolder
pour ajouter un en-tête au-dessus des éléments affichés dansRecyclerView
.
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.
- 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.
- Ouvrez SleepNightAdapter.kt.
- Sous la classe
SleepNightListener
, au niveau supérieur, définissez une classesealed
nomméeDataItem
qui représente un élément de données.
Une classesealed
définit un type fermé, ce qui signifie que toutes les sous-classes deDataItem
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 deDataItem
qui pourrait casser votre adaptateur.
sealed class DataItem {
}
- 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 uneSleepNightItem
, qui est un wrapper autour d'unSleepNight
. Elle prend donc une seule valeur appeléesleepNight
. Pour l'intégrer à la classe scellée, faites-la étendreDataItem
.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- 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 commeobject
. Cela signifie qu'il n'y aura jamais qu'une seule instance deHeader
. Une fois encore, faites-le étendreDataItem
.
object Header: DataItem()
- Dans
DataItem
, au niveau de la classe, définissez une propriétéabstract
Long
nomméeid
. Lorsque l'adaptateur utiliseDiffUtil
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, carSleepNightItem
etHeader
doivent remplacer la propriété abstraiteid
.
abstract val id: Long
- Dans
SleepNightItem
, remplacezid
pour renvoyernightId
.
override val id = sleepNight.nightId
- Dans
Header
, remplacezid
pour renvoyerLong.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 unnightId
existant.
override val id = Long.MIN_VALUE
- 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
- 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" />
- Extrayez
"Sleep Results"
dans une ressource de chaîne et appelez-laheader_text
.
<string name="header_text">Sleep Results</string>
- Dans SleepNightAdapter.kt, à l'intérieur de
SleepNightAdapter
, au-dessus de la classeViewHolder
, créez une classeTextViewHolder
. Cette classe augmente la mise en page textview.xml et renvoie une instanceTextViewHolder
. Comme vous l'avez déjà fait, voici le code. Vous devrez importerView
etR
:
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
- Dans
SleepNightAdapter.kt
, au niveau supérieur, sous les instructionsimport
et au-dessus deSleepNightAdapter
, définissez deux constantes pour les types de vues.
LeRecyclerView
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
- Dans
SleepNightAdapter
, créez une fonction pour remplacergetItemViewType()
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
- Dans la définition de
SleepNightAdapter
, remplacez le premier argument deListAdapter
parDataItem
au lieu deSleepNight
. - Dans la définition de
SleepNightAdapter
, remplacez le deuxième argument générique deListAdapter
parRecyclerView.ViewHolder
au lieu deSleepNightAdapter.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()
- Modifiez la signature de
onCreateViewHolder()
pour renvoyer unRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- 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()
- Remplacez le type de paramètre de
onBindViewHolder()
(ViewHolder
) parRecyclerView.ViewHolder
.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- Ajoutez une condition pour n'attribuer des données au support de vue que si celui-ci est un
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Castez le type d'objet renvoyé par
getItem()
surDataItem.SleepNightItem
. Votre fonctiononBindViewHolder()
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
- Modifiez les méthodes dans
SleepNightDiffCallback
pour qu'elles utilisent votre nouvelle classeDataItem
au lieu deSleepNight
. 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
- Dans
SleepNightAdapter
, sousonCreateViewHolder()
, définissez une fonctionaddHeaderAndSubmitList()
comme indiqué ci-dessous. Cette fonction accepte une liste deSleepNight
. Au lieu d'utilisersubmitList()
, fourni parListAdapter
, pour envoyer votre liste, vous utiliserez cette fonction pour ajouter un en-tête, puis envoyer la liste.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- Dans
addHeaderAndSubmitList()
, si la liste transmise estnull
, 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)
- Ouvrez SleepTrackerFragment.kt et remplacez l'appel à
submitList()
paraddHeaderAndSubmitList()
.
- 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 :
- Au niveau supérieur de la classe
SleepNightAdapter
, définissez unCoroutineScope
avecDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- Dans
addHeaderAndSubmitList()
, lancez une coroutine dansadapterScope
pour manipuler la liste. Passez ensuite au contexteDispatchers.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)
}
}
}
- 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.
- Ouvrez SleepTrackerFragment.kt.
- Recherchez le code où vous définissez
manager
, vers la fin deonCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Sous
manager
, définissezmanager.spanSizeLookup
, comme indiqué. Vous devez créer unobject
, carsetSpanSizeLookup
n'accepte pas de lambda. Pour créer unobject
en Kotlin, saisissezobject : classname
, dans ce casGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- 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) ouAlt+Enter
(Windows) pour appliquer l'appel du constructeur.
- Une erreur s'affichera alors sur
object
, indiquant que vous devez remplacer des méthodes. Placez le curseur surobject
, appuyez surOption+Enter
(Mac) ouAlt+Enter
(Windows) pour ouvrir le menu des intentions, puis remplacez la méthodegetSpanSize()
.
- 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
}
}
- 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"
- 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. LeAdapter
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 classeDataItem
. - Créez une fonction
addHeaderAndSubmitList()
qui utilise des coroutines pour ajouter l'en-tête à l'ensemble de données, puis appellesubmitList()
. - Implémentez
GridLayoutManager.SpanSizeLookup()
pour que seul l'en-tête s'étende sur trois colonnes.
Cours Udacity :
- Développer des applications Android avec Kotlin
- Kotlin Bootcamp for Programmers (Formation Kotlin pour les programmeurs)
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 :
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.