1. Bienvenue
Introduction
Dans l'atelier de programmation précédent, vous avez appris à extraire des données d'un service Web et à analyser la réponse dans un objet Kotlin. Dans cet atelier, vous allez vous appuyer sur ces connaissances pour charger et afficher des photos à partir d'une adresse URL. Vous verrez également à nouveau comment créer une RecyclerView
et l'utiliser pour afficher une grille d'images sur la page de présentation.
Conditions préalables
- Vous savez créer et utiliser des fragments.
- Vous savez récupérer des fichiers JSON à partir d'un service Web REST et analyser ces données dans des objets Kotlin à l'aide des bibliothèques Retrofit et Moshi.
- Vous savez créer une mise en page basée sur une grille avec un élément
RecyclerView
. - Vous connaissez le fonctionnement des éléments
Adapter
,ViewHolder
etDiffUtil
.
Points abordés
- Comment utiliser la bibliothèque Coil pour charger et afficher une image à partir d'une adresse URL
- Comment utiliser un élément
RecyclerView
et un adaptateur pour afficher une grille d'images - Comment gérer les erreurs potentielles lors du téléchargement et de l'affichage des images
Objectifs de l'atelier
- Modifier l'application Mars Photos pour obtenir l'URL de l'image à partir des données de Mars, puis utiliser Coil pour charger et afficher cette image
- Ajouter une animation de chargement et une icône d'erreur à l'application.
- Utiliser un élément
RecyclerView
pour afficher une grille d'images de Mars. - Ajouter un état et une gestion des erreurs à
RecyclerView
.
Ce dont vous avez besoin
- Un ordinateur doté d'un navigateur Web récent, par exemple la dernière version de Chrome
- Un accès Internet sur votre ordinateur
2. Présentation de l'application
Dans cet atelier de programmation, vous allez reprendre l'application de l'atelier précédent, MarsPhotos. L'application MarsPhotos se connecte à un service Web pour récupérer et afficher le nombre d'objets Kotlin récupérés à l'aide de Retrofit. Ces objets Kotlin contiennent les URL des photos réelles de la surface de Mars prises par les rovers de la NASA.
La version de l'application que vous allez créer dans cet atelier de programmation remplit la page de présentation en affichant des photos de Mars sous la forme d'une grille d'images. Les images font partie des données que votre application a récupérées sur le service Web Mars. Votre application utilisera la bibliothèque Coil pour charger et afficher les images, ainsi qu'un élément RecyclerView
pour les mettre en page. Votre application pourra également gérer correctement les erreurs réseau.
3. Afficher une image depuis Internet
Même si l'affichage d'une photo à partir d'une URL peut sembler simple, certaines manipulations sont requises pour que l'image fonctionne correctement. Vous devez la télécharger, la stocker dans la mémoire interne et décoder son format compressé pour récupérer une image compatible avec Android. L'image doit être mise en cache dans la mémoire, sur un support de stockage ou les deux. Toutes ces opérations doivent être réalisées dans des threads de faible priorité afin que l'UI reste réactive. Pour optimiser les performances du réseau et du processeur, vous pouvez également extraire et décoder plusieurs images à la fois.
Heureusement, vous pouvez utiliser Coil, une bibliothèque développée par la communauté, pour télécharger, mettre en mémoire tampon, décoder et mettre en cache vos images. Si vous décidez de vous passer de Coil, vous aurez beaucoup plus de travail.
Coil a besoin de deux choses :
- l'URL de l'image que vous souhaitez charger et afficher ;
- un objet
ImageView
pour afficher cette image.
Dans cette tâche, vous allez apprendre à utiliser Coil pour afficher une seule image à partir du service Web Mars. Vous afficherez l'image de la première photo de Mars dans la liste renvoyée par le service Web. Voici les captures d'écran avant et après :
Ajouter la dépendance Coil
- Ouvrez l'application de solution MarsPhotos de l'atelier de programmation précédent.
- Exécutez l'application pour voir comment elle réagit. (Elle affiche le nombre total de photos de Mars extraites.)
- Ouvrez build.gradle (Module: app).
- Dans la section
dependencies
(dépendances), ajoutez la ligne suivante pour la bibliothèque Coil :
// Coil
implementation "io.coil-kt:coil:1.1.1"
Sur la page de documentation de la bibliothèque Coil, vérifiez quelle est la dernière version et faites la mise à jour.
- La bibliothèque Coil est hébergée et disponible dans le dépôt
mavenCentral()
. Dans build.gradle (Project: MarsPhotos), ajoutezmavenCentral()
dans le blocrepositories
, en haut.
repositories {
google()
mavenCentral()
}
- Cliquez sur Sync now (Synchroniser) pour recréer le projet avec la nouvelle dépendance.
Mettre à jour le ViewModel
Au cours de cette étape, vous allez ajouter une propriété LiveData
à la classe OverviewViewModel
pour stocker l'objet Kotlin reçu, MarsPhoto.
- Ouvrez
overview/OverviewViewModel.kt
. Juste en dessous de la déclaration de la propriété_status
, ajoutez une propriété modifiable appelée_photos
, de typeMutableLiveData
, qui peut stocker un unique objetMarsPhoto
.
private val _photos = MutableLiveData<MarsPhoto>()
Lorsque vous y êtes invité, importez com.example.android.marsphotos.network.MarsPhoto
.
- Juste en dessous de la déclaration
_photos
, ajoutez un champ de support public nomméphotos
, de typeLiveData<MarsPhoto>
.
val photos: LiveData<MarsPhoto> = _photos
- Dans la méthode
getMarsPhotos()
, dans le bloctry{}
, recherchez la ligne qui définit les données extraites du service Web surlistResult.
try {
val listResult = MarsApi.retrofitService.getPhotos()
...
}
- Attribuez la première photo de Mars extraite à la nouvelle variable
_photos
. ModifiezlistResult
pour lui attribuer_photos.value
. Attribuez l'URL de la première photo à l'index0
. Cela génèrera une erreur que vous corrigerez plus tard.
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
...
}
- Sur la ligne suivante, remplacez
status.value
par ce qui suit. Utilisez les données de la nouvelle propriété au lieu delistResult
. Affichez l'URL de la première image de la liste.
try {
...
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- Le bloc
try{}
complet ressemble maintenant à ceci :
try {
_photos.value = MarsApi.retrofitService.getPhotos()[0]
_status.value = " First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
- Exécutez l'application. L'élément
TextView
affiche désormais l'URL de la première photo de Mars. Pour l'instant, vous avez configuré les élémentsViewModel
etLiveData
pour cette URL.
Utiliser des adaptateurs de liaison
Les adaptateurs de liaison sont des méthodes annotées qui permettent de créer des setters adaptés aux propriétés personnalisées de votre vue.
Généralement, en XML, vous définissez un attribut à l'aide du code suivant : android:text="Sample Text"
. Le système Android recherche alors automatiquement un setter portant le même nom que l'attribut text
, qui est défini par la méthode setText(String: text)
. La méthode setText(String: text)
est une méthode setter pour certaines vues fournies par le framework Android. Vous pouvez obtenir un comportement similaire en personnalisant des adaptateurs de liaison : il est alors nécessaire de fournir un attribut et une logique personnalisés qui seront appelés par la bibliothèque Data Binding.
Exemple :
Pour effectuer une opération plus complexe que d'appeler simplement un setter sur l'élément ImageView (vue image), vous devez définir une image drawable. Il est conseillé de ne pas utiliser le thread UI (thread principal) pour charger des images depuis Internet. Choisissez d'abord un attribut personnalisé pour attribuer l'image à un élément ImageView
. Dans l'exemple suivant, il s'agit de imageUrl
.
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{product.imageUrl}"/>
Si vous n'ajoutez pas de code supplémentaire, le système recherche une méthode setImageUrl(String)
sur ImageView
et ne la trouve pas, ce qui génère une erreur : cet attribut personnalisé n'est pas fourni par le framework. Vous devez créer un moyen d'implémenter et de définir l'attribut app:imageUrl
sur ImageView
. Pour ce faire, vous allez utiliser des adaptateurs de liaison (méthodes annotées).
Exemple d'adaptateur de liaison :
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
// Load the image in the background using Coil.
}
}
}
L'annotation @BindingAdapter
utilise le nom de l'attribut comme paramètre.
Dans la méthode bindImage
, le premier paramètre correspond au type de vue cible, et le second à la valeur définie pour l'attribut.
Dans la méthode, la bibliothèque Coil charge l'image en dehors du thread UI et la définit dans ImageView
.
Créer un adaptateur de liaison et utiliser Coil
- Dans le package
com.example.android.marsphotos
, créez un fichier Kotlin appeléBindingAdapters
. Ce fichier contiendra les adaptateurs de liaison que vous utilisez dans l'application.
- Dans
BindingAdapters.kt
, créez une fonctionbindImage()
en tant que fonction de niveau supérieur (et non dans une classe) qui reçoit les paramètresImageView
etString
.
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
Lorsque vous y êtes invité, importez android.widget.ImageView
.
- Annotez la fonction avec
@BindingAdapter
. L'annotation@BindingAdapter
indique à la liaison de données d'exécuter cet adaptateur de liaison lorsqu'une vue possède l'attributimageUrl
.
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
}
Lorsque vous y êtes invité, importez androidx.databinding.BindingAdapter
.
Fonction de portée "let"
let
est l'une des cinq fonctions de champ d'application en Kotlin. Elle permet d'exécuter un bloc de code dans le contexte d'un objet. Pour en savoir plus sur les fonctions de champ d'application, consultez la documentation.
Utilisation :
let
permet d'appeler une ou plusieurs fonctions sur des résultats de chaînes d'appel.
La fonction let
et l'opérateur d'appel sécurisé (?.
) permettent d'effectuer une opération null-safe sur l'objet. Dans ce cas, le bloc de code let
ne sera exécuté que si l'objet n'est pas nul.
- Dans la fonction
bindImage()
, ajoutez un bloclet{}
à l'argumentimgUrl
à l'aide de l'opérateur d'appel sécurisé.
imgUrl?.let {
}
- Dans le bloc
let{}
, ajoutez la ligne suivante pour convertir l'URL en objetUri
à l'aide de la méthodetoUri()
. Pour utiliser le schéma HTTPS, ajoutezbuildUpon.scheme("https")
au compilateurtoUri
. Appelezbuild()
pour créer l'objet.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
Lorsque vous y êtes invité, importez androidx.core.net.toUri
.
- Dans le bloc
let{}
, après la déclarationimgUri
, utilisezload(){}
à partir de Coil pour charger l'image de l'objetimgUri
dansimgView
.
imgView.load(imgUri) {
}
Lorsque vous y êtes invité, importez coil.load
.
- Une fois terminée, votre méthode doit se présenter comme suit :
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri)
}
}
Mettre à jour la mise en page et les fragments
Dans la section précédente, vous avez utilisé la bibliothèque Coil pour charger l'image. Pour afficher l'image à l'écran, l'étape suivante consiste à mettre à jour l'élément ImageView
avec le nouvel attribut afin d'afficher une seule image.
Plus tard dans l'atelier de programmation, vous utiliserez res/layout/grid_view_item.xml
comme fichier de ressources de mise en page pour chaque élément de la grille de RecyclerView
. Dans cette tâche, vous allez utiliser ce fichier temporairement pour afficher l'image à l'aide de l'URL récupérée lors de la tâche précédente. Ce fichier de mise en page remplacera temporairement fragment_overview.xml
.
- Ouvrez
res/layout/grid_view_item.xml
. - Au-dessus de l'élément
<ImageView>
, ajoutez un élément<data>
pour la liaison de données et liez la classeOverviewViewModel
:
<data>
<variable
name="viewModel"
type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
- Ajoutez un attribut
app:imageUrl
à l'élémentImageView
pour utiliser le nouvel adaptateur de liaison pour le chargement d'image. Gardez à l'esprit quephotos
contient une listeMarsPhotos
récupérée à partir du serveur. Attribuez la première URL d'entrée à l'attributimageUrl
.
<ImageView
android:id="@+id/mars_image"
...
app:imageUrl="@{viewModel.photos.imgSrcUrl}"
... />
- Ouvrez
overview/OverviewFragment.kt
. Dans la méthodeonCreateView()
, mettez en commentaire la ligne qui gonfle la classeFragmentOverviewBinding
et l'attribue à la variable de liaison. Une fois cette ligne retirée, des erreurs s'afficheront. Ce n'est que temporaire. vous les corrigerez plus tard.
//val binding = FragmentOverviewBinding.inflate(inflater)
- Utilisez
grid_view_item.xml
au lieu defragment_overview.xml.
Ajoutez ensuite la ligne suivante pour gonfler la classeGridViewItemBinding
.
val binding = GridViewItemBinding.inflate(inflater)
Si vous y êtes invité, importez com.example.android.marsphotos. databinding.GridViewItemBinding
.
- Exécutez l'application. Vous devriez maintenant voir une seule image de Mars.
Ajouter des images de chargement et d'erreur
Coil vous permet d'améliorer l'expérience utilisateur en utilisant une image qui réserve l'espace lors du chargement, ou une image d'erreur en cas d'échec. Cette fonctionnalité est particulièrement utile si l'image est manquante ou corrompue, par exemple. Au cours de cette étape, vous allez ajouter cette fonctionnalité à l'adaptateur de liaison.
- Ouvrez
res/drawable/ic_broken_image.xml
, puis cliquez sur l'onglet Conception à droite. Pour l'image d'erreur, nous allons utiliser l'icône d'image défectueuse disponible dans la bibliothèque d'icônes intégrée. Ce drawable vectoriel utilise l'attributandroid:tint
pour colorer l'icône en gris.
- Ouvrez
res/drawable/loading_animation.xml
. Ce drawable est une animation qui fait pivoter une image drawable,loading_img.xml
, autour de son point central. (L'animation ne s'affiche pas dans l'aperçu.)
- Revenez au fichier
BindingAdapters.kt
. Dans la méthodebindImage()
, mettez à jour l'appel àimgView.
load
(imgUri)
pour ajouter un lambda de fin comme suit. Ce code définit l'image qui remplit l'espace lors du chargement (le drawableloading_animation
). Il définit également une image à utiliser en cas d'échec du chargement (le drawablebroken_image
).
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
- Une fois terminée, la méthode
bindImage()
se présente comme suit :
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView, imgUrl: String?) {
imgUrl?.let {
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()
imgView.load(imgUri) {
placeholder(R.drawable.loading_animation)
error(R.drawable.ic_broken_image)
}
}
}
- Exécutez l'application. Selon la vitesse de votre connexion réseau, vous pouvez voir brièvement l'image de chargement pendant que Coil télécharge et affiche l'image de propriété. Toutefois, pour le moment, l'icône représentant une image défectueuse ne s'affichera pas même si vous désactivez votre réseau. Vous allez résoudre ce problème dans la dernière tâche de cet atelier de programmation.
- Annulez les modifications temporaires apportées à
overview/OverviewFragment.kt
. Dans la méthodeonCreateview()
, annulez la mise en commentaire de la ligne qui gonfleFragmentOverviewBinding
. Supprimez ou mettez en commentaire la ligne qui gonfleGridViewIteMBinding
.
val binding = FragmentOverviewBinding.inflate(inflater)
// val binding = GridViewItemBinding.inflate(inflater)
4. Afficher une grille d'images avec RecyclerView
Votre application charge désormais une photo de Mars depuis Internet. En utilisant les données du premier élément de la liste MarsPhoto
, vous avez créé une propriété LiveData
dans l'élément ViewModel
, puis vous avez utilisé l'URL de cette photo de Mars pour insérer un élément ImageView
. L'objectif étant que votre application affiche une grille d'images, au cours de cette tâche, vous allez utiliser un élément RecyclerView
avec un gestionnaire de mise en page adapté pour afficher des images sous la forme d'une grille.
Mettre à jour le ViewModel
Dans la tâche précédente, dans OverviewViewModel
, vous avez ajouté un objet LiveData
appelé _photos
qui contient un objet MarsPhoto
(le premier élément de la liste des réponses du service Web). Au cours de cette étape, vous allez modifier ce LiveData
pour qu'il puisse contenir l'ensemble des objets MarsPhoto
.
- Ouvrez
overview/OverviewViewModel.kt
. - Modifiez le type de
_photos
pour le définir sur la liste d'objetsMarsPhoto
.
private val _photos = MutableLiveData<List<MarsPhoto>>()
- Remplacez également le type de la propriété de support
photos
parList<MarsPhoto>
:
val photos: LiveData<List<MarsPhoto>> = _photos
- Faites défiler la page jusqu'au bloc
try {}
dans la méthodegetMarsPhotos()
.MarsApi.
retrofitService
.getPhotos()
renvoie une liste d'objets MarsPhoto
, que vous pouvez simplement attribuer à _photos.value
.
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
- Le bloc
try/catch
complet ressemble maintenant à ceci :
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = "Success: Mars properties retrieved"
} catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
Mise en page sous forme de grille
Le gestionnaire GridLayoutManager
pour RecyclerView
présente les données sous la forme d'une grille que l'utilisateur peut faire défiler, comme illustré ci-dessous.
Du point de vue du design, la mise en page sous forme de grille est idéale pour les listes qui peuvent être représentées par des icônes ou des images, comme c'est le cas pour votre application, car elle permet d'afficher des photos de Mars.
Fonctionnement de la mise en page sous forme de grille
La mise en page en grille organise les éléments en lignes et en colonnes. En cas de défilement vertical, chaque élément d'une ligne occupe un "segment". Un élément peut occuper plusieurs segments. Dans le cas ci-dessous, un segment équivaut à la largeur d'une colonne, ce qui permet d'afficher trois segments.
Dans les deux exemples ci-dessous, chaque ligne est composée de trois segments. Par défaut, le gestionnaire GridLayoutManager
assigne chaque élément dans un segment, dans la limite du nombre de segments que vous indiquez. Lorsqu'il atteint ce nombre, il envoie le segment suivant à la ligne suivante.
Ajouter un RecyclerView
Au cours de cette étape, vous allez modifier la mise en page de l'application pour utiliser un élément RecyclerView (vue de recyclage) avec une mise en page en grille, plutôt qu'une vue composée d'une seule image.
- Ouvrez
layout/grid_view_item.xml
. Supprimez la variable de donnéesviewModel
. - Dans la balise
<data>
, ajoutez la variablephoto
suivante, de typeMarsPhoto
.
<data>
<variable
name="photo"
type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
- Dans
<ImageView>
, modifiez l'attributapp:imageUrl
pour faire référence à l'URL de l'image dans l'objetMarsPhoto
. Ces modifications annulent les modifications temporaires apportées lors de la tâche précédente.
app:imageUrl="@{photo.imgSrcUrl}"
- Ouvrez
layout/fragment_overview.xml
. Supprimez l'intégralité de l'élément<TextView>
. - À sa place, ajoutez l'élément
<RecyclerView>
suivant. Définissez l'ID surphotos_grid
, puis les attributswidth
(largeur) etheight
(hauteur) sur0dp
pour remplir leConstraintLayout
parent. Vous allez utiliser une mise en page en mode grille, vous devez donc définir l'attributlayoutManager
surandroidx.recyclerview.widget.GridLayoutManager
. DéfinissezspanCount
sur2
pour avoir deux colonnes.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
app:layoutManager=
"androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2" />
- Pour voir un aperçu du résultat généré par le code ci-dessus dans la vue Conception, utilisez
tools:itemCount
afin de définir le nombre d'éléments affichés dans notre mise en page sur16
. L'attributitemCount
indique le nombre d'éléments que l'éditeur de mise en page doit afficher dans la fenêtre Aperçu. Définissez la mise en page des éléments de la liste surgrid_view_item
à l'aide detools:listitem
.
<androidx.recyclerview.widget.RecyclerView
...
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
- Passez à la vue Conception. Vous devriez voir un aperçu qui ressemble à la capture d'écran suivante. Nous n'en sommes pas encore aux photos de Mars, mais vous pouvez voir à quoi ressemblera la mise en page à l'aide de l'élément RecyclerView. L'aperçu utilise la marge intérieure et la mise en page
grid_view_item
pour chaque élément de la grillerecyclerview
.
- Conformément aux consignes Material Design, vous devez avoir un espace de
8dp
en haut, en bas et sur les côtés de la liste, et un espace de4dp
entre chaque élément. Pour ce faire, utilisez une combinaison de marges intérieures dans les mises en pagefragment_overview.xml
etgrid_view_item.xml
.
- Ouvrez
layout/gridview_item.xml
. Remarquez que la valeur de l'attributpadding
est déjà égale à2dp
, ce qui définit la marge intérieure, située entre l'extérieur de l'élément et le contenu. Vous disposerez ainsi d'un espace de4dp
entre le contenu de chaque élément et de2dp
le long des bords extérieurs. Cela signifie que nous avons besoin d'une marge intérieure supplémentaire de6dp
sur les bords extérieurs pour respecter les consignes de conception. - Revenez à
layout/fragment_overview.xml
. Ajoutez6dp
de marge intérieure à l'élémentRecyclerView
afin d'obtenir un total de8dp
à l'extérieur et de4dp
à l'intérieur, comme demandé dans les consignes.
<androidx.recyclerview.widget.RecyclerView
...
android:padding="6dp"
... />
- L'élément
<RecyclerView>
complet doit se présenter comme suit.
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photos_grid"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="6dp"
app:layoutManager=
"androidx.recyclerview.widget.GridLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:spanCount="2"
tools:itemCount="16"
tools:listitem="@layout/grid_view_item" />
Ajouter l'adaptateur pour grille de photos
La mise en page fragment_overview
présente maintenant un élément RecyclerView
avec une mise en page sous forme de grille. Au cours de cette étape, vous allez faire le lien entre le RecyclerView
et les données extraites du serveur Web via un adaptateur RecyclerView
.
ListAdapter (rappel)
ListAdapter
est une sous-classe de la classe RecyclerView.Adapter
. Elle permet de présenter des données de liste dans un élément RecyclerView
tout en intégrant les différences de calcul entre les listes sur un thread en arrière-plan.
Dans cette application, vous allez utiliser l'implémentation de DiffUtil
dans ListAdapter.
L'avantage de DiffUtil
est qu'à chaque fois qu'un élément de RecyclerView
est ajouté, modifié ou supprimé, la liste n'est pas entièrement actualisée. Seuls les éléments concernés sont actualisés.
Ajoutez ListAdapter
à votre application.
- Dans le package
overview
, créez une classe Kotlin appeléePhotoGridAdapter
. - Étendez la classe
PhotoGridAdapter
à partir deListAdapter
avec les paramètres de constructeur indiqués ci-dessous. La classePhotoGridAdapter
étendListAdapter
, dont le constructeur a besoin d'un élément de type liste, du conteneur de vue et d'une implémentationDiffUtil.ItemCallback
.
class PhotoGridAdapter : ListAdapter<MarsPhoto,
PhotoGridAdapter.MarsPhotoViewHolder>(DiffCallback) {
}
Si vous y êtes invité, importez les classes androidx.recyclerview.widget.ListAdapter
et com.example.android.marsphoto.network.MarsPhoto
. Dans les étapes suivantes, vous allez ajouter les autres implémentations manquantes de ce constructeur qui génèrent des erreurs.
- Pour résoudre les erreurs ci-dessus, vous allez ajouter les méthodes dont vous avez besoin à cette étape, puis vous les implémenterez plus tard dans cette tâche. Cliquez sur la classe
PhotoGridAdapter
, puis sur l'ampoule rouge. Dans le menu déroulant, sélectionnez Implémenter des membres. Dans la fenêtre pop-up qui s'affiche, sélectionnez les méthodesListAdapter
,onCreateViewHolder()
etonBindViewHolder()
. Android Studio continuera d'afficher les erreurs, mais vous les corrigerez définitivement plus loin dans cette tâche.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoGridAdapter.MarsPhotoViewHolder {
TODO("Not yet implemented")
}
override fun onBindViewHolder(holder: PhotoGridAdapter.MarsPhotoViewHolder, position: Int) {
TODO("Not yet implemented")
}
Pour implémenter les méthodes onCreateViewHolder
et onBindViewHolder
, vous avez besoin de MarsPhotoViewHolder
, que vous ajouterez à l'étape suivante.
- Dans
PhotoGridAdapter
, ajoutez une définition de classe interne pourMarsPhotoViewHolder
, qui étendRecyclerView.ViewHolder
. Vous avez besoin de la variableGridViewItemBinding
pour lierMarsPhoto
à la mise en page. Pour ce faire, transmettez-la dansMarsPhotoViewHolder
. La classeViewHolder
d'origine nécessite une vue dans son constructeur. Utilisez la vue de liaison à la racine pour la transmettre.
class MarsPhotoViewHolder(private var binding:
GridViewItemBinding):
RecyclerView.ViewHolder(binding.root) {
}
Si vous y êtes invité, importez androidx.recyclerview.widget.RecyclerView
et com.example.android.marsrealestate.databinding.GridViewItemBinding
.
- Dans
MarsPhotoViewHolder
, créez une méthodebind()
qui reçoit un objetMarsPhoto
comme argument et définitbinding.property
sur cet objet. Après avoir défini la propriété, appelezexecutePendingBindings()
. La mise à jour se lance immédiatement.
fun bind(MarsPhoto: MarsPhoto) {
binding.photo = MarsPhoto
binding.executePendingBindings()
}
- Toujours dans la classe
PhotoGridAdapter
deonCreateViewHolder()
, supprimez l'élément TODO et ajoutez la ligne ci-dessous. La méthodeonCreateViewHolder()
doit renvoyer un nouveauMarsPhotoViewHolder
, créé en gonflantGridViewItemBinding
et en utilisant leLayoutInflater
de votre contexteViewGroup
parent.
return MarsPhotoViewHolder(GridViewItemBinding.inflate(
LayoutInflater.from(parent.context)))
Si vous y êtes invité, importez android.view.LayoutInflater
.
- Dans la méthode
onBindViewHolder()
, supprimez l'élément TODO et ajoutez les lignes ci-dessous. Ici, vous appelezgetItem()
pour obtenir l'objetMarsPhoto
associé à la position actuelle deRecyclerView
, puis vous transmettez cette propriété à la méthodebind()
dansMarsPhotoViewHolder
.
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
- Dans
PhotoGridAdapter
, ajoutez une définition d'objet associé pourDiffCallback
, comme indiqué ci-dessous.
L'objetDiffCallback
étendDiffUtil.ItemCallback
avec le type générique de l'objet que vous souhaitez comparer, à savoirMarsPhoto
. Dans cette implémentation, vous allez comparer deux objets qui sont des photos de Mars.
companion object DiffCallback : DiffUtil.ItemCallback<MarsPhoto>() {
}
Lorsque vous y êtes invité, importez androidx.recyclerview.widget.DiffUtil
.
- Appuyez sur l'ampoule rouge pour implémenter les méthodes de comparaison pour l'objet
DiffCallback
, à savoirareItemsTheSame()
etareContentsTheSame()
.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented")
}
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
TODO("Not yet implemented") }
- Dans la méthode
areItemsTheSame()
, supprimez l'élémentTODO
. Cette méthode est appelée parDiffUtil
pour déterminer si deux objets représentent le même élément.DiffUtil
utilise cette méthode pour déterminer si le nouvel objetMarsPhoto
est identique à l'ancien objetMarsPhoto
. L'ID de chaque élément (objetMarsPhoto
) est unique. Comparez les ID deoldItem
et denewItem
, puis renvoyez le résultat.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.id == newItem.id
}
- Dans
areContentsTheSame()
, supprimez l'élémentTODO
. Cette méthode est appelée parDiffUtil
lorsqu'elle souhaite vérifier si deux éléments comportent les mêmes données. Dans le cas de l'élément MarsPhoto, l'élément essentiel est l'URL de l'image. Comparez les URL deoldItem
et denewItem
, puis renvoyez le résultat.
override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
return oldItem.imgSrcUrl == newItem.imgSrcUrl
}
Vérifiez que vous pouvez compiler et exécuter l'application sans erreur. Normalement, l'émulateur affiche un écran vide. L'élément RecyclerView est prêt, mais aucune donnée ne lui est transmise. Vous allez corriger ce problème à l'étape suivante.
Ajouter l'adaptateur de liaison et connecter les composants
Au cours de cette étape, vous allez utiliser BindingAdapter
pour initialiser PhotoGridAdapter
avec la liste d'objets MarsPhoto
. Si vous utilisez BindingAdapter
pour définir les données de RecyclerView
, la liaison de données observe automatiquement LiveData
pour trouver la liste des objets MarsPhoto
. Ensuite, l'adaptateur de liaison est appelé automatiquement lorsque la liste MarsPhoto
change.
- Ouvrez
BindingAdapters.kt
. - À la fin du fichier, ajoutez une méthode
bindRecyclerView()
qui accepte un élémentRecyclerView
et une liste d'objetsMarsPhoto
comme arguments. Annotez cette méthode avec@BindingAdapter
et l'attributlistData
.
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
}
Si vous y êtes invité, importez androidx.recyclerview.widget.RecyclerView
et com.example.android.marsphotos.network.MarsPhoto
.
- Dans la fonction
bindRecyclerView()
, convertissezrecyclerView.adapter
enPhotoGridAdapter
et affectez-le à une nouvelle propriétéval
, à savoiradapter.
.
val adapter = recyclerView.adapter as PhotoGridAdapter
- À la fin de la fonction
bindRecyclerView()
, appelezadapter.submitList()
avec les données de la liste de photos de Mars. Cela permet de prévenir l'élémentRecyclerView
lorsqu'une nouvelle liste est disponible.
adapter.submitList(data)
Si vous y êtes invité, importez com.example.android.marsrealestate.overview.PhotoGridAdapter
.
- Une fois terminé, l'adaptateur de liaison
bindRecyclerView
doit se présenter comme suit :
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView,
data: List<MarsPhoto>?) {
val adapter = recyclerView.adapter as PhotoGridAdapter
adapter.submitList(data)
}
- Pour tout connecter, ouvrez
res/layout/fragment_overview.xml
. Ajoutez l'attributapp:listData
à l'élémentRecyclerView
et définissez-le surviewmodel.photos
à l'aide de la liaison de données. Ce processus est semblable à celui que vous avez effectué pourImageView
lors d'une tâche précédente.
app:listData="@{viewModel.photos}"
- Ouvrez
overview/OverviewFragment.kt
. DansonCreateView()
, juste avant l'instructionreturn
, initialisez l'adaptateurRecyclerView
dansbinding.photosGrid
sur un nouvel objetPhotoGridAdapter
.
binding.photosGrid.adapter = PhotoGridAdapter()
- Exécutez l'application. Une grille d'images de Mars doit s'afficher. En faisant défiler la page pour afficher de nouvelles images, elles apparaissent de façon étrange. La marge intérieure reste en haut et en bas de l'élément
RecyclerView
lors du défilement. Par conséquent, il semble que vous ne faites jamais vraiment défiler la liste sous la barre d'action.
- Pour résoudre ce problème, vous devez indiquer à l'élément
RecyclerView
de ne pas rogner le contenu interne en fonction de la marge intérieure à l'aide de l'attribut android:clipToPadding. Cela vous permet de faire défiler l'affichage dans la zone de marge intérieure. Revenez àlayout/fragment_overview.xml
. Ajoutez l'attributandroid:clipToPadding
àRecyclerView
, puis définissez-le surfalse
.
<androidx.recyclerview.widget.RecyclerView
...
android:clipToPadding="false"
... />
- Exécutez votre application. Notez également qu'une icône de chargement en cours s'affiche avant que l'image apparaisse, comme prévu. Il s'agit de l'image de chargement que vous avez transmise à la bibliothèque Coil.
- Pendant l'exécution de l'application, activez le mode Avion. Faites défiler les images dans l'émulateur. À la place des images qui n'ont pas encore été chargées, des icônes d'images défectueuses s'affichent. Il s'agit de l'image drawable que vous avez transmise à la bibliothèque d'images Coil et qui s'affiche si une erreur réseau survient ou si une image n'a pas pu être extraite.
Félicitations, vous avez presque terminé ! Dans la prochaine tâche, qui sera la dernière, vous allez améliorer l'expérience utilisateur en ajoutant de meilleurs systèmes de gestion des erreurs à l'application.
5. Ajouter une gestion des erreurs dans RecyclerView
Lorsqu'une image ne peut pas être extraite, l'application MarsPhotos affiche l'icône d'image défectueuse. Mais en l'absence de connexion réseau, l'application affiche un écran vierge. Au cours de l'étape suivante, vous allez vérifier que ce comportement survient comme prévu.
- Activez le mode Avion sur votre appareil ou émulateur. Exécutez l'application à partir d'Android Studio. L'écran est effectivement vierge.
En termes d'expérience utilisateur, il est possible de faire bien mieux. Dans cette tâche, vous allez ajouter une gestion des erreurs basique dans le but de mieux informer l'utilisateur du problème. Si aucune connexion Internet n'est disponible, l'application devra afficher une icône d'erreur de connexion, et lorsqu'elle sera en train d'extraire la liste MarsPhoto
, elle devra afficher une animation de chargement.
Ajouter un état à ViewModel
Dans cette tâche, vous allez créer une propriété dans OverviewViewModel
pour représenter l'état de la requête Web. Trois états sont possibles : chargement en cours, réussite et échec. L'état "chargement en cours" est appliqué lorsque l'application est en attente de données. L'état "réussite" indique que les données ont bien été récupérées auprès du service Web. L'état "échec" indique des erreurs de réseau ou de connexion.
Classes d'énumération en Kotlin
Pour représenter ces trois états dans votre application, vous utiliserez enum
. enum
est l'abréviation de "énumération", c'est-à-dire une liste numérotée de tous les éléments d'une collection. Chaque constante enum
est un objet de la classe du même nom (enum
).
En Kotlin, enum
est un type de données qui peut contenir un ensemble de constantes. Pour définir ce type de données, ajoutez le mot clé enum
devant une définition de classe, comme indiqué ci-dessous. Les constantes d'énumération sont séparées par des virgules.
Définition :
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
Utilisation :
var direction : Direction = Direction.NORTH
Comme indiqué ci-dessus, vous pouvez faire référence aux objets enum
à l'aide du nom de la classe suivi d'un opérateur point (.) et du nom de la constante.
Ajoutez la définition de classe d'énumération avec les valeurs d'état dans ViewModel.
- Ouvrez
overview/OverviewViewModel.kt
. En haut du fichier (après les importations, avant la définition de la classe), ajoutezenum
pour représenter tous les états disponibles :
enum class MarsApiStatus { LOADING, ERROR, DONE }
- Faites défiler la page jusqu'à la définition des propriétés
_status
etstatus
. Remplacez les typesString
parMarsApiStatus. MarsApiStatus
est la classe d'énumération que vous avez définie à l'étape précédente.
private val _status = MutableLiveData<MarsApiStatus>()
val status: LiveData<MarsApiStatus> = _status
- Dans la méthode
getMarsPhotos()
, remplacez la chaîne"Success: ..."
par l'étatMarsApiStatus.DONE
et la chaîne"Failure..."
parMarsApiStatus.ERROR
.
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
}
- Au-dessus du bloc
try {}
, définissez l'état surMarsApiStatus.LOADING
. Il s'agit de l'état initial appliqué pendant que la coroutine est en cours d'exécution et que l'application est en attente de données. Une fois terminé, le blocviewModelScope.launch
{}
ressemble à ceci :
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
}
}
- Après l'état d'erreur dans le bloc
catch {}
, définissez_photos
sur une liste vide. Cette action efface le contenu de l'élément RecyclerView.
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
- Une fois terminée, la méthode
getMarsPhotos()
doit se présenter comme suit :
private fun getMarsPhotos() {
viewModelScope.launch {
_status.value = MarsApiStatus.LOADING
try {
_photos.value = MarsApi.retrofitService.getPhotos()
_status.value = MarsApiStatus.DONE
} catch (e: Exception) {
_status.value = MarsApiStatus.ERROR
_photos.value = listOf()
}
}
}
Vous avez défini des états d'énumération en fonction de l'état d'exécution et fixé l'état de chargement au début de la coroutine. Vous avez également défini l'état comme terminé lorsque votre application a fini de récupérer les données sur le serveur Web, et défini une erreur en cas d'exception. Dans la tâche suivante, vous allez utiliser un adaptateur de liaison pour afficher les icônes correspondantes.
Ajouter un adaptateur de liaison pour l'ImageView de l'état
Vous avez configuré MarsApiStatus
dans OverviewViewModel
à l'aide d'un ensemble d'états enum
. Au cours de cette étape, vous allez l'afficher dans l'application. Vous utilisez un adaptateur de liaison pour un élément ImageView
afin d'afficher les icônes des états de chargement et d'erreur. Lorsque l'état de l'application est "en cours de chargement" ou "erreur", l'élément ImageView
doit être visible. Une fois le chargement terminé, l'élément ImageView
doit être rendu invisible.
- Ouvrez
BindingAdapters.kt
et faites défiler la page jusqu'à la fin du fichier pour ajouter un autre adaptateur. Ajoutez un adaptateur de liaison nommébindStatus()
, qui reçoitImageView
etMarsApiStatus
comme arguments. Annotez la méthode avec@BindingAdapter
en transmettant l'attribut personnalisémarsApiStatus
comme paramètre.
@BindingAdapter("marsApiStatus")
fun bindStatus(statusImageView: ImageView,
status: MarsApiStatus?) {
}
Si vous y êtes invité, importez com.example.android.marsrealestate.overview.MarsApiStatus
.
- Ajoutez un bloc
when {}
dans la méthodebindStatus()
pour passer d'un état à un autre.
when (status) {
}
- Dans
when {}
, ajoutez un cas pour l'état de chargement (MarsApiStatus.LOADING
). Pour cet état, définissezImageView
sur "VISIBLE", puis attribuez-lui l'animation de chargement. Il s'agit du drawable animé que vous avez utilisé pour Coil lors de la tâche précédente.
when (status) {
MarsApiStatus.LOADING -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.loading_animation)
}
}
Si vous y êtes invité, importez android.view.View
.
- Ajoutez un cas pour l'état d'erreur, à savoir
MarsApiStatus.ERROR
. Comme vous l'avez fait pour l'étatLOADING
, définissez l'étatImageView
sur "VISIBLE" et utilisez le drawable d'erreur de connexion.
MarsApiStatus.ERROR -> {
statusImageView.visibility = View.VISIBLE
statusImageView.setImageResource(R.drawable.ic_connection_error)
}
- Ajoutez un cas pour l'état "réussite", à savoir
MarsApiStatus.DONE
. Dans ce cas, vous obtenez une réponse positive. Par conséquent, vous devez définir la visibilité de l'ImageView
de l'état surView.
GONE
pour le masquer.
MarsApiStatus.DONE -> {
statusImageView.visibility = View.GONE
}
Vous avez configuré l'adaptateur de liaison pour gérer l'ImageView de l'état. À l'étape suivante, vous allez ajouter un élément ImageView qui utilise le nouvel adaptateur de liaison.
Ajouter l'ImageView de l'état
À cette étape, vous allez ajouter l'élément ImageView (vue image) dans le fichier fragment_overview.xml
. Il affichera alors l'état que vous avez défini précédemment.
- Ouvrez
res/layout/fragment_overview.xml
. Dans le conteneurConstraintLayout
, sous l'élémentRecyclerView
, ajoutez l'élémentImageView
indiqué ci-dessous.
<ImageView
android:id="@+id/status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:marsApiStatus="@{viewModel.status}" />
L'élément ImageView
ci-dessus présente les mêmes contraintes que RecyclerView
. Cependant, la largeur et la hauteur utilisent wrap_content
pour centrer l'image au lieu de l'agrandir pour remplir l'affichage. Vous pouvez également remarquer l'attribut app:marsApiStatus
défini sur viewModel.status
, qui appelle votre BindingAdapter
(adaptateur de liaison) lorsque la propriété d'état dans ViewModel
change.
- Pour tester le code ci-dessus, simulez une erreur de connexion réseau en activant le mode Avion dans votre émulateur ou sur votre appareil. Compilez et exécutez l'application. L'image d'erreur devrait s'afficher :
- Appuyez sur le bouton "Retour" pour fermer l'application, puis désactivez le mode Avion. Utilisez l'écran "Récents" pour revenir à l'application. Selon la vitesse de votre connexion réseau, une icône de chargement peut s'afficher très brièvement lorsque l'application interroge le service Web, avant que les images s'affichent.
Bravo ! Vous avez terminé cet atelier de programmation et l'application MarsPhotos est prête à l'emploi. Il est temps d'en profiter pour montrer de vraies photos de Mars à vos proches.
6. Code de solution
Le code de solution de cet atelier de programmation figure dans le projet ci-dessous. Utilisez la branche main pour extraire ou télécharger le code.
Pour obtenir le code de cet atelier de programmation et l'ouvrir dans Android Studio, procédez comme suit :
Obtenir le code
- Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
- Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.
- Dans la boîte de dialogue, cliquez sur le bouton Download ZIP (Télécharger le fichier ZIP) pour enregistrer le projet sur votre ordinateur. Attendez la fin du téléchargement.
- Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
- Double-cliquez sur le fichier ZIP pour le décompresser. Un dossier contenant les fichiers du projet est alors créé.
Ouvrir le projet dans Android Studio
- Lancez Android Studio.
- Dans la fenêtre Welcome to Android Studio (Bienvenue dans Android Studio), cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).
Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).
- Dans la boîte de dialogue Import Project (Importer un projet), accédez à l'emplacement du dossier du projet décompressé. Il se trouve probablement dans le dossier Téléchargements.
- Double-cliquez sur le dossier de ce projet.
- Attendez qu'Android Studio ouvre le projet.
- Cliquez sur le bouton Run (Exécuter) pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
- Parcourez les fichiers du projet dans la fenêtre de l'outil Projet pour voir comment l'application est configurée.
7. Résumé
- La bibliothèque Coil simplifie le processus de gestion des images, par exemple le téléchargement, la mise en mémoire tampon, le décodage et la mise en cache dans votre application.
- Les adaptateurs de liaison sont des méthodes d'extension situées entre une vue et les données qui lui sont liées. Ils permettent d'indiquer un comportement personnalisé en cas de modification des données, par exemple pour appeler Coil afin de charger une image à partir d'une URL dans un élément
ImageView
. - Les adaptateurs de liaison sont des méthodes d'extension annotées avec
@BindingAdapter
. - Pour afficher une grille d'images, utilisez un
RecyclerView
avec un gestionnaireGridLayoutManager
. - Pour mettre à jour la liste des propriétés lorsqu'elle est modifiée, utilisez un adaptateur de liaison entre
RecyclerView
et la mise en page.
8. En savoir plus
Documentation pour les développeurs Android :
- Présentation de ViewModel
- Présentation de LiveData
- Coroutines, documentation officielle
- Adaptateurs de liaison
Autre :