Charger et afficher des images depuis Internet

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 et DiffUtil.

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.

243d21747dfb8999.png

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

  1. Ouvrez l'application de solution MarsPhotos de l'atelier de programmation précédent.
  2. Exécutez l'application pour voir comment elle réagit. (Elle affiche le nombre total de photos de Mars extraites.)
  3. Ouvrez build.gradle (Module: app).
  4. 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.

  1. La bibliothèque Coil est hébergée et disponible dans le dépôt mavenCentral(). Dans build.gradle (Project: MarsPhotos), ajoutez mavenCentral() dans le bloc repositories, en haut.
repositories {
   google()
   mavenCentral()
}
  1. 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.

  1. 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 type MutableLiveData, qui peut stocker un unique objet MarsPhoto.
private val _photos = MutableLiveData<MarsPhoto>()

Lorsque vous y êtes invité, importez com.example.android.marsphotos.network.MarsPhoto.

  1. Juste en dessous de la déclaration _photos, ajoutez un champ de support public nommé photos, de type LiveData<MarsPhoto>.
val photos: LiveData<MarsPhoto> = _photos
  1. Dans la méthode getMarsPhotos(), dans le bloc try{}, recherchez la ligne qui définit les données extraites du service Web sur listResult.
try {
   val listResult = MarsApi.retrofitService.getPhotos()
   ...
}
  1. Attribuez la première photo de Mars extraite à la nouvelle variable _photos. Modifiez listResult pour lui attribuer _photos.value. Attribuez l'URL de la première photo à l'index 0. Cela génèrera une erreur que vous corrigerez plus tard.
try {
   _photos.value = MarsApi.retrofitService.getPhotos()[0]
   ...
}
  1. Sur la ligne suivante, remplacez status.value par ce qui suit. Utilisez les données de la nouvelle propriété au lieu de listResult. Affichez l'URL de la première image de la liste.
try {
   ...
   _status.value = "   First Mars image URL : ${_photos.value!!.imgSrcUrl}"

}
  1. Le bloc try{} complet ressemble maintenant à ceci :
try {
   _photos.value = MarsApi.retrofitService.getPhotos()[0]
   _status.value = "   First Mars image URL : ${_photos.value!!.imgSrcUrl}"
}
  1. 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éments ViewModel et LiveData pour cette URL.

ae99ec8569198456.png

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

  1. 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.

a04afbd6ae8ccfcd.png

  1. Dans BindingAdapters.kt, créez une fonction bindImage() en tant que fonction de niveau supérieur (et non dans une classe) qui reçoit les paramètres ImageView et String.
fun bindImage(imgView: ImageView, imgUrl: String?) {

}

Lorsque vous y êtes invité, importez android.widget.ImageView.

  1. 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'attribut imageUrl.
@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.

  1. Dans la fonction bindImage(), ajoutez un bloc let{} à l'argument imgUrl à l'aide de l'opérateur d'appel sécurisé.
imgUrl?.let {
}
  1. Dans le bloc let{}, ajoutez la ligne suivante pour convertir l'URL en objet Uri à l'aide de la méthode toUri(). Pour utiliser le schéma HTTPS, ajoutez buildUpon.scheme("https") au compilateur toUri. Appelez build() pour créer l'objet.
val imgUri = imgUrl.toUri().buildUpon().scheme("https").build()

Lorsque vous y êtes invité, importez androidx.core.net.toUri.

  1. Dans le bloc let{}, après la déclaration imgUri, utilisez load(){} à partir de Coil pour charger l'image de l'objet imgUri dans imgView.
imgView.load(imgUri) {
}

Lorsque vous y êtes invité, importez coil.load.

  1. 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.

  1. Ouvrez res/layout/grid_view_item.xml.
  2. Au-dessus de l'élément <ImageView>, ajoutez un élément <data> pour la liaison de données et liez la classe OverviewViewModel :
<data>
   <variable
       name="viewModel"
       type="com.example.android.marsphotos.overview.OverviewViewModel" />
</data>
  1. Ajoutez un attribut app:imageUrl à l'élément ImageView pour utiliser le nouvel adaptateur de liaison pour le chargement d'image. Gardez à l'esprit que photos contient une liste MarsPhotos récupérée à partir du serveur. Attribuez la première URL d'entrée à l'attribut imageUrl.
    <ImageView
        android:id="@+id/mars_image"
        ...
        app:imageUrl="@{viewModel.photos.imgSrcUrl}"
        ... />
  1. Ouvrez overview/OverviewFragment.kt. Dans la méthode onCreateView(), mettez en commentaire la ligne qui gonfle la classe FragmentOverviewBinding 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)
  1. Utilisez grid_view_item.xml au lieu de fragment_overview.xml. Ajoutez ensuite la ligne suivante pour gonfler la classe GridViewItemBinding.
val binding = GridViewItemBinding.inflate(inflater)

Si vous y êtes invité, importez com.example.android.marsphotos. databinding.GridViewItemBinding.

  1. Exécutez l'application. Vous devriez maintenant voir une seule image de Mars.

e59b6e849e63ae2b.png

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.

  1. 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'attribut android:tint pour colorer l'icône en gris.

467c213c859e1904.png

  1. 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.)

6c1f87d1c932c762.png

  1. Revenez au fichier BindingAdapters.kt. Dans la méthode bindImage(), 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 drawable loading_animation). Il définit également une image à utiliser en cas d'échec du chargement (le drawable broken_image).
imgView.load(imgUri) {
   placeholder(R.drawable.loading_animation)
   error(R.drawable.ic_broken_image)
}
  1. 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)
        }
    }
}
  1. 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.

6dcecd205a0741a.gif

  1. Annulez les modifications temporaires apportées à overview/OverviewFragment.kt. Dans la méthode onCreateview(), annulez la mise en commentaire de la ligne qui gonfle FragmentOverviewBinding. Supprimez ou mettez en commentaire la ligne qui gonfle GridViewIteMBinding.
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.

  1. Ouvrez overview/OverviewViewModel.kt.
  2. Modifiez le type de _photos pour le définir sur la liste d'objets MarsPhoto.
private val _photos = MutableLiveData<List<MarsPhoto>>()
  1. Remplacez également le type de la propriété de support photos par List<MarsPhoto> :
 val photos: LiveData<List<MarsPhoto>> = _photos
  1. Faites défiler la page jusqu'au bloc try {} dans la méthode getMarsPhotos(). 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"
  1. 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.

fcf0fc4b78f8650.png

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.

  1. Ouvrez layout/grid_view_item.xml. Supprimez la variable de données viewModel.
  2. Dans la balise <data>, ajoutez la variable photo suivante, de type MarsPhoto.
<data>
   <variable
       name="photo"
       type="com.example.android.marsphotos.network.MarsPhoto" />
</data>
  1. Dans <ImageView>, modifiez l'attribut app:imageUrl pour faire référence à l'URL de l'image dans l'objet MarsPhoto. Ces modifications annulent les modifications temporaires apportées lors de la tâche précédente.
app:imageUrl="@{photo.imgSrcUrl}"
  1. Ouvrez layout/fragment_overview.xml. Supprimez l'intégralité de l'élément <TextView>.
  2. À sa place, ajoutez l'élément <RecyclerView> suivant. Définissez l'ID sur photos_grid, puis les attributs width (largeur) et height (hauteur) sur 0dp pour remplir le ConstraintLayout parent. Vous allez utiliser une mise en page en mode grille, vous devez donc définir l'attribut layoutManager sur androidx.recyclerview.widget.GridLayoutManager. Définissez spanCount sur 2 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" />
  1. 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 sur 16. L'attribut itemCount 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 sur grid_view_item à l'aide de tools:listitem.
<androidx.recyclerview.widget.RecyclerView
            ...
            tools:itemCount="16"
            tools:listitem="@layout/grid_view_item" />
  1. 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 grille recyclerview.

20742824367c3952.png

  1. 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 de 4dp entre chaque élément. Pour ce faire, utilisez une combinaison de marges intérieures dans les mises en page fragment_overview.xml et grid_view_item.xml.

a3561fa85fea7a8f.png

  1. Ouvrez layout/gridview_item.xml. Remarquez que la valeur de l'attribut padding 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 de 4dp entre le contenu de chaque élément et de 2dp le long des bords extérieurs. Cela signifie que nous avons besoin d'une marge intérieure supplémentaire de 6dp sur les bords extérieurs pour respecter les consignes de conception.
  2. Revenez à layout/fragment_overview.xml. Ajoutez 6dp de marge intérieure à l'élément RecyclerView afin d'obtenir un total de 8dp à l'extérieur et de 4dp à l'intérieur, comme demandé dans les consignes.
<androidx.recyclerview.widget.RecyclerView
            ...
            android:padding="6dp"
            ...  />
  1. 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.

  1. Dans le package overview, créez une classe Kotlin appelée PhotoGridAdapter.
  2. Étendez la classe PhotoGridAdapter à partir de ListAdapter avec les paramètres de constructeur indiqués ci-dessous. La classe PhotoGridAdapter étend ListAdapter, dont le constructeur a besoin d'un élément de type liste, du conteneur de vue et d'une implémentation DiffUtil.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.

  1. 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éthodes ListAdapter, onCreateViewHolder() et onBindViewHolder(). 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.

  1. Dans PhotoGridAdapter, ajoutez une définition de classe interne pour MarsPhotoViewHolder, qui étend RecyclerView.ViewHolder. Vous avez besoin de la variable GridViewItemBinding pour lier MarsPhoto à la mise en page. Pour ce faire, transmettez-la dans MarsPhotoViewHolder. La classe ViewHolder 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.

  1. Dans MarsPhotoViewHolder, créez une méthode bind() qui reçoit un objet MarsPhoto comme argument et définit binding.property sur cet objet. Après avoir défini la propriété, appelez executePendingBindings(). La mise à jour se lance immédiatement.
fun bind(MarsPhoto: MarsPhoto) {
   binding.photo = MarsPhoto
   binding.executePendingBindings()
}
  1. Toujours dans la classe PhotoGridAdapter de onCreateViewHolder(), supprimez l'élément TODO et ajoutez la ligne ci-dessous. La méthode onCreateViewHolder() doit renvoyer un nouveau MarsPhotoViewHolder, créé en gonflant GridViewItemBinding et en utilisant le LayoutInflater de votre contexte ViewGroup parent.
   return MarsPhotoViewHolder(GridViewItemBinding.inflate(
      LayoutInflater.from(parent.context)))

Si vous y êtes invité, importez android.view.LayoutInflater.

  1. Dans la méthode onBindViewHolder(), supprimez l'élément TODO et ajoutez les lignes ci-dessous. Ici, vous appelez getItem() pour obtenir l'objet MarsPhoto associé à la position actuelle de RecyclerView, puis vous transmettez cette propriété à la méthode bind() dans MarsPhotoViewHolder.
val marsPhoto = getItem(position)
holder.bind(marsPhoto)
  1. Dans PhotoGridAdapter, ajoutez une définition d'objet associé pour DiffCallback, comme indiqué ci-dessous.
    L'objet DiffCallback étend DiffUtil.ItemCallback avec le type générique de l'objet que vous souhaitez comparer, à savoir MarsPhoto. 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.

  1. Appuyez sur l'ampoule rouge pour implémenter les méthodes de comparaison pour l'objet DiffCallback, à savoir areItemsTheSame() et areContentsTheSame().
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   TODO("Not yet implemented")
}

override fun areContentsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   TODO("Not yet implemented") }
  1. Dans la méthode areItemsTheSame(), supprimez l'élément TODO. Cette méthode est appelée par DiffUtil pour déterminer si deux objets représentent le même élément. DiffUtil utilise cette méthode pour déterminer si le nouvel objet MarsPhoto est identique à l'ancien objet MarsPhoto. L'ID de chaque élément (objet MarsPhoto) est unique. Comparez les ID de oldItem et de newItem, puis renvoyez le résultat.
override fun areItemsTheSame(oldItem: MarsPhoto, newItem: MarsPhoto): Boolean {
   return oldItem.id == newItem.id
}
  1. Dans areContentsTheSame(), supprimez l'élément TODO. Cette méthode est appelée par DiffUtil 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 de oldItem et de newItem, 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.

  1. Ouvrez BindingAdapters.kt.
  2. À la fin du fichier, ajoutez une méthode bindRecyclerView() qui accepte un élément RecyclerView et une liste d'objets MarsPhoto comme arguments. Annotez cette méthode avec @BindingAdapter et l'attribut listData.
@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.

  1. Dans la fonction bindRecyclerView(), convertissez recyclerView.adapter en PhotoGridAdapter et affectez-le à une nouvelle propriété val, à savoir adapter..
val adapter = recyclerView.adapter as PhotoGridAdapter
  1. À la fin de la fonction bindRecyclerView(), appelez adapter.submitList() avec les données de la liste de photos de Mars. Cela permet de prévenir l'élément RecyclerView lorsqu'une nouvelle liste est disponible.
adapter.submitList(data)

Si vous y êtes invité, importez com.example.android.marsrealestate.overview.PhotoGridAdapter.

  1. 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)

}
  1. Pour tout connecter, ouvrez res/layout/fragment_overview.xml. Ajoutez l'attribut app:listData à l'élément RecyclerView et définissez-le sur viewmodel.photos à l'aide de la liaison de données. Ce processus est semblable à celui que vous avez effectué pour ImageView lors d'une tâche précédente.
app:listData="@{viewModel.photos}"
  1. Ouvrez overview/OverviewFragment.kt. Dans onCreateView(), juste avant l'instruction return, initialisez l'adaptateur RecyclerView dans binding.photosGrid sur un nouvel objet PhotoGridAdapter.
binding.photosGrid.adapter = PhotoGridAdapter()
  1. 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.

5d03641aa1589842.png

  1. 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'attribut android:clipToPadding à RecyclerView, puis définissez-le sur false.
<androidx.recyclerview.widget.RecyclerView
            ...
            android:clipToPadding="false"
            ...  />
  1. 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.

3128b84aa22ef97e.png

  1. 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.

28d2cbba564f35ff.png

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.

  1. Activez le mode Avion sur votre appareil ou émulateur. Exécutez l'application à partir d'Android Studio. L'écran est effectivement vierge.

492011786c2dd7f7.png

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.

  1. Ouvrez overview/OverviewViewModel.kt. En haut du fichier (après les importations, avant la définition de la classe), ajoutez enum pour représenter tous les états disponibles :
enum class MarsApiStatus { LOADING, ERROR, DONE }
  1. Faites défiler la page jusqu'à la définition des propriétés _status et status. Remplacez les types String par MarsApiStatus. 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
  1. Dans la méthode getMarsPhotos(), remplacez la chaîne "Success: ..." par l'état MarsApiStatus.DONE et la chaîne "Failure..." par MarsApiStatus.ERROR.
try {
    _photos.value = MarsApi.retrofitService.getPhotos()
    _status.value = MarsApiStatus.DONE
} catch (e: Exception) {
     _status.value = MarsApiStatus.ERROR
}
  1. Au-dessus du bloc try {}, définissez l'état sur MarsApiStatus.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 bloc viewModelScope.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
            }
        }
  1. 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()
}
  1. 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.

  1. 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çoit ImageView et MarsApiStatus 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.

  1. Ajoutez un bloc when {} dans la méthode bindStatus() pour passer d'un état à un autre.
when (status) {

}
  1. Dans when {}, ajoutez un cas pour l'état de chargement (MarsApiStatus.LOADING). Pour cet état, définissez ImageView 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.

  1. Ajoutez un cas pour l'état d'erreur, à savoir MarsApiStatus.ERROR. Comme vous l'avez fait pour l'état LOADING, définissez l'état ImageView sur "VISIBLE" et utilisez le drawable d'erreur de connexion.
MarsApiStatus.ERROR -> {
   statusImageView.visibility = View.VISIBLE
   statusImageView.setImageResource(R.drawable.ic_connection_error)
}
  1. 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 sur View.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.

  1. Ouvrez res/layout/fragment_overview.xml. Dans le conteneur ConstraintLayout, sous l'élément RecyclerView, ajoutez l'élément ImageView 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.

  1. 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 :

a91ddb1c89f2efec.png

  1. 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

  1. Cliquez sur l'URL indiquée. La page GitHub du projet s'ouvre dans un navigateur.
  2. Sur la page GitHub du projet, cliquez sur le bouton Code pour afficher une boîte de dialogue.

5b0a76c50478a73f.png

  1. 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.
  2. Recherchez le fichier sur votre ordinateur (il se trouve probablement dans le dossier Téléchargements).
  3. 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

  1. Lancez Android Studio.
  2. 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).

36cc44fcf0f89a1d.png

Remarque : Si Android Studio est déjà ouvert, sélectionnez l'option de menu File > New > Import Project (Fichier > Nouveau > Importer un projet).

21f3eec988dcfbe9.png

  1. 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.
  2. Double-cliquez sur le dossier de ce projet.
  3. Attendez qu'Android Studio ouvre le projet.
  4. Cliquez sur le bouton Run (Exécuter) 11c34fc5e516fb1c.png pour créer et exécuter l'application. Assurez-vous qu'elle fonctionne correctement.
  5. 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 gestionnaire GridLayoutManager.
  • 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 :

Autre :