Récupérer des données sur Internet

1. Avant de commencer

La plupart des applications Android du marché se connectent à Internet pour effectuer des opérations réseau, par exemple pour récupérer des e-mails, des messages ou d'autres informations auprès d'un serveur backend. Gmail, YouTube et Google Photos, par exemple, se connectent à Internet pour afficher les données utilisateur.

Dans cet atelier de programmation, vous allez utiliser des bibliothèques Open Source développées par la communauté pour créer une couche de données et obtenir des données auprès d'un serveur backend. Cette approche simplifie grandement l'extraction des données et aide également l'application à suivre les bonnes pratiques Android, comme effectuer des opérations sur un thread en arrière-plan. Vous afficherez aussi un message d'erreur si la connexion Internet est lente ou indisponible, pour informer l'utilisateur d'éventuels problèmes de connectivité réseau.

Conditions préalables

  • Vous disposez de connaissances de base sur la création de fonctions modulables.
  • Vous disposez de connaissances de base sur l'utilisation des composants d'architecture Android ViewModel.
  • Vous disposez de connaissances de base sur l'utilisation de coroutines pour des tâches de longue durée.
  • Vous disposez de connaissances de base sur l'ajout de dépendances dans build.gradle.kts.

Points abordés

  • Définition d'un service Web REST
  • Utilisation de la bibliothèque Retrofit pour se connecter à un service Web REST via Internet et obtenir une réponse
  • Utilisation de la bibliothèque Sérialisation (kotlinx.serialization) pour analyser la réponse JSON sous forme d'objet de données

Objectifs de l'atelier

  • Modifier une application de démarrage pour effectuer une requête API de service Web et gérer la réponse
  • Implémenter une couche de données pour votre application à l'aide de la bibliothèque Retrofit
  • Analyser la réponse JSON du service Web dans la liste des objets de données de votre application avec la bibliothèque kotlinx.serialization, puis l'associer à l'état de l'UI
  • Utiliser la prise en charge des coroutines de Retrofit pour simplifier le code

Ce dont vous avez besoin

  • Un ordinateur avec Android Studio
  • Le code de démarrage de l'application Mars Photos

2. Présentation de l'application

Vous utilisez l'application Mars Photos, qui affiche des images de la surface de Mars. Cette application se connecte à un service Web pour récupérer et afficher les photos de Mars. Les images sont de vraies photos de Mars prises par les rovers de la NASA. L'image ci-dessous est une capture d'écran de l'application finale, qui contient une grille d'images.

68f4ff12cc1e2d81.png

La version de l'application que vous allez créer a peu d'intérêt visuel. Cet atelier de programmation se concentre sur la couche de données de l'application, pour la connexion à Internet et le téléchargement des données brutes de propriété via un service Web. Pour que l'application récupère et analyse correctement ces données, vous pouvez imprimer le nombre de photos reçues du serveur backend dans un composable Text.

a59e55909b6e9213.png

3. Découverte de l'application de démarrage Mars Photos

Télécharger le code de démarrage

Pour commencer, téléchargez le code de démarrage :

Vous pouvez également cloner le dépôt GitHub du code :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout starter

Vous pouvez parcourir le code dans le dépôt GitHub Mars Photos.

Exécuter le code de démarrage

  1. Ouvrez le projet téléchargé dans Android Studio. Le nom du dossier du projet est basic-android-kotlin-compose-training-mars-photos.
  2. Dans le volet Android, développez app > java. Notez que l'application comporte un dossier de package nommé ui. Il s'agit de la couche d'UI de l'application.

d5102d14c37fc433.png

  1. Exécutez l'application. Lorsque vous compilez et exécutez l'application, l'écran suivant s'affiche avec un espace réservé en son centre. À la fin de cet atelier de programmation, vous allez modifier le texte de cet espace réservé en le remplaçant par le nombre de photos récupérées.

95328ffbc9d7104b.png

Tutoriel du code de démarrage

Dans cette tâche, vous allez vous familiariser avec la structure du projet. Les listes suivantes présentent les fichiers et dossiers importants du projet.

ui\MarsPhotosApp.kt :

  • Ce fichier contient le composable MarsPhotosApp, qui affiche le contenu à l'écran, par exemple la barre supérieure de l'application et le composable HomeScreen. Le texte d'espace réservé de l'étape précédente s'affiche dans ce composable.
  • Dans l'atelier de programmation suivant, ce composable affiche les données reçues en provenance du serveur backend des photos de Mars.

screens\MarsViewModel.kt :

  • Ce fichier est le modèle de visualisation correspondant à MarsPhotosApp.
  • Cette classe contient une propriété MutableState nommée marsUiState. Si vous modifiez la valeur de cette propriété, le texte d'espace réservé qui s'affiche à l'écran est mis à jour.
  • La méthode getMarsPhotos() met à jour la réponse de l'espace réservé. Plus tard dans l'atelier de programmation, vous utiliserez cette méthode pour afficher les données extraites du serveur. L'objectif de cet atelier de programmation est de mettre à jour la propriété ViewModel du MutableState à l'aide de données issues d'Internet.

screens\HomeScreen.kt :

  • Ce fichier contient les composables HomeScreen et ResultScreen. ResultScreen présente une mise en page Box simple qui affiche la valeur de marsUiState dans un composable Text.

MainActivity.kt :

  • La seule tâche de cette activité consiste à charger le ViewModel et à afficher le composable MarsPhotosApp.

4. Présentation des services Web

Dans cet atelier de programmation, vous créez une couche pour le service réseau qui communique avec le serveur backend et récupère les données requises. Vous utilisez une bibliothèque tierce appelée Retrofit pour implémenter cette tâche. Nous y reviendrons plus tard. Le ViewModel communique avec la couche de données, et le reste de l'application est transparent pour cette implémentation.

3fe849669bd308da.png

MarsViewModel est chargé de passer l'appel réseau pour obtenir les données des photos de Mars. Dans le ViewModel, utilisez MutableState pour mettre à jour l'UI de l'application lorsque les données changent.

5. Services Web et Retrofit

Les données des photos de Mars sont stockées sur un serveur Web. Pour intégrer ces données dans votre application, vous devez établir une connexion et communiquer avec le serveur sur Internet.

301162f0dca12fcf.png

9d5543c4c7e85e9f.png

Aujourd'hui, la plupart des serveurs Web exécutent des services Web à l'aide d'une architecture Web sans état courante, appelée REST, qui signifie REpresentational StateTransfer. Les services Web qui proposent cette architecture sont appelés services RESTful.

Les requêtes sont envoyées aux services Web RESTful de manière standardisée, via des URI (Uniform Resource Identifiers). Un URI identifie une ressource sur le serveur grâce à son nom, sans indiquer son emplacement ni la manière d'y accéder. Par exemple, dans l'application de cette leçon, vous récupérez les URL des images à l'aide de l'URI de serveur suivant (ce serveur héberge à la fois Mars Real Estate et Mars Photos) :

android-kotlin-fun-mars-server.appspot.com

Une URL (Uniform Resource Locator) est un sous-ensemble d'un URI qui spécifie l'emplacement d'une ressource et le mécanisme permettant de la récupérer.

Exemple :

L'URL suivante permet d'obtenir la liste des biens immobiliers disponibles sur Mars :

https://android-kotlin-fun-mars-server.appspot.com/realestate

L'URL suivante permet d'obtenir une liste des photos de Mars :

https://android-kotlin-fun-mars-server.appspot.com/photos

Ces URL font référence à une ressource identifiée, par exemple /realestate ou /photos, qui peut être obtenue via le protocole de transfert hypertexte (http:) du réseau. Dans cet atelier de programmation, vous utilisez le point de terminaison /photos. Un point de terminaison est une URL qui vous permet d'accéder à un service Web exécuté sur un serveur.

Requête de service Web

Chaque requête de service Web contient un URI et est transférée au serveur à l'aide du même protocole HTTP que celui utilisé par les navigateurs Web, comme Chrome. Les requêtes HTTP contiennent une opération indiquant au serveur ce qu'il doit faire.

Voici quelques opérations HTTP courantes :

  • GET, pour récupérer les données du serveur
  • POST, pour créer des données sur le serveur
  • PUT, pour mettre à jour des données existantes sur le serveur
  • DELETE, pour supprimer des données du serveur

Votre application envoie une demande GET HTTP au serveur pour obtenir les informations sur les photos de Mars, puis le serveur renvoie une réponse à votre application, qui contient les URL des images.

5bbeef4ded3e84cf.png

83e8a6eb79249ebe.png

La réponse d'un service Web est mise en forme dans un format de données courant comme XML (eXtensible Markup Language) ou JSON (JavaScript Object Notation). Le format JSON représente des données structurées sous forme de paires clé/valeur. Une application communique avec l'API REST via JSON, que vous découvrirez dans une prochaine tâche.

Dans cette tâche, vous allez établir une connexion réseau avec le serveur, communiquer avec celui-ci et recevoir une réponse JSON. Vous utiliserez un serveur backend déjà créé. Dans cet atelier de programmation, vous utilisez la bibliothèque tierce Retrofit pour communiquer avec le serveur backend.

Bibliothèques externes

Les bibliothèques externes (ou bibliothèques tierces) sont comme des extensions des principales API d'Android. Les bibliothèques que vous utilisez dans ce cours sont Open Source, développées par la communauté et gérées par les contributions collectives de l'immense communauté Android du monde entier. Ces ressources aident les développeurs Android à créer de meilleures applications.

Bibliothèque Retrofit

La bibliothèque Retrofit que vous utilisez dans cet atelier de programmation pour communiquer avec le service Web RESTful Mars est un bon exemple de bibliothèque bien gérée et alimentée. Vous pouvez le constater par vous-même en passant en revue les problèmes en cours (certains sont des demandes de fonctionnalités) et les problèmes clôturés figurant sur la page GitHub. Si les développeurs résolvent régulièrement les problèmes et répondent aux demandes de fonctionnalités, cela signifie que cette bibliothèque est probablement bien gérée et qu'elle peut être utilisée dans votre application. Pour en savoir plus sur la bibliothèque, vous pouvez aussi consulter la documentation Retrofit.

La bibliothèque Retrofit communique avec le backend REST. Elle génère le code, mais vous devez fournir les URI du service Web en fonction des paramètres que nous lui transmettons. Vous en saurez plus à ce sujet dans les sections suivantes.

26043df178401c6a.png

Ajouter des dépendances Retrofit

Android Gradle vous permet d'ajouter des bibliothèques externes à votre projet. En plus de la dépendance de la bibliothèque, vous devez également inclure le dépôt où elle est hébergée.

  1. Ouvrez le fichier Gradle build.gradle.kts (Module :app) au niveau du module.
  2. Dans la section dependencies, ajoutez les lignes suivantes pour les bibliothèques Retrofit :
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
// Retrofit with Scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")

Les deux bibliothèques fonctionnent ensemble. La première dépendance est pour la bibliothèque Retrofit2 elle-même, et la seconde pour le convertisseur scalaire Retrofit. Retrofit2 est la version mise à jour de la bibliothèque Retrofit. Ce convertisseur scalaire permet à Retrofit de renvoyer le résultat JSON sous forme de String. JSON est un format de stockage et de transport de données entre client et serveur. Vous découvrirez JSON plus en détail dans une section ultérieure.

  1. Cliquez sur Synchroniser pour créer à nouveau le projet avec les nouvelles dépendances.

6. Connexion à Internet

Vous utilisez la bibliothèque Retrofit pour communiquer avec le service Web Mars et afficher la réponse JSON brute sous forme de String. L'espace réservé Text affiche la chaîne de réponse JSON renvoyée ou un message indiquant une erreur de connexion.

Retrofit crée une API réseau pour l'application en fonction du contenu du service Web. Les données du service Web sont récupérées et acheminées via une bibliothèque de conversions distincte qui sait comment décoder les données et les renvoyer sous la forme d'objets tels que String. Retrofit accepte les formats de données courants comme XML et JSON. Au final, Retrofit crée le code qui appelle et utilise ce service à votre place, y compris les détails essentiels tels que l'exécution des requêtes sur des threads en arrière-plan.

a78435c405c1e347.png

Dans cette tâche, vous ajoutez une couche de données au projet Mars Photos que votre ViewModel utilise pour communiquer avec le service Web. Suivez cette procédure pour implémenter l'API de service Retrofit :

  • Créez une source de données, à savoir la classe MarsApiService.
  • Créez un objet Retrofit avec l'URL de base et la fabrique de conversions permettant de convertir des chaînes.
  • Créez une interface expliquant comment Retrofit communique avec le serveur Web.
  • Créez un service Retrofit et exposez l'instance au service d'API pour le reste de l'application.

Procédez comme suit :

  1. Effectuez un clic droit sur le package com.example.marsphotos dans le volet de votre projet Android, puis sélectionnez New > Package (Nouveau > Package).
  2. Dans la fenêtre pop-up, ajoutez network à la fin du nom de package suggéré.
  3. Créez un fichier Kotlin sous le nouveau package. Nommez-le MarsApiService.
  4. Ouvrez network/MarsApiService.kt.
  5. Ajoutez la constante suivante pour l'URL de base du service Web.
private const val BASE_URL =
   "https://android-kotlin-fun-mars-server.appspot.com"
  1. Ajoutez un compilateur Retrofit juste en dessous de cette constante pour compiler et créer un objet Retrofit.
import retrofit2.Retrofit

private val retrofit = Retrofit.Builder()

Pour créer une API de services Web, Retrofit a besoin de l'URI de base du service Web et d'une fabrique de conversions. Le convertisseur indique à Retrofit comment traiter les données qu'il récupère auprès du service Web. Dans ce cas, vous souhaitez que Retrofit récupère une réponse JSON à partir du service Web et la renvoie sous forme de String. Retrofit comporte un convertisseur scalaire ScalarsConverter prenant en charge les chaînes et d'autres types primitifs.

  1. Appelez addConverterFactory() sur le compilateur avec une instance de ScalarsConverterFactory.
import retrofit2.converter.scalars.ScalarsConverterFactory

private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
  1. Ajoutez l'URL de base du service Web à l'aide de la méthode baseUrl().
  2. Appelez build() pour créer l'objet Retrofit.
private val retrofit = Retrofit.Builder()
   .addConverterFactory(ScalarsConverterFactory.create())
   .baseUrl(BASE_URL)
   .build()
  1. Sous l'appel du compilateur Retrofit, définissez une interface appelée MarsApiService, qui définit la façon dont Retrofit communique avec le serveur Web à l'aide de requêtes HTTP.
interface MarsApiService {
}
  1. Ajoutez une fonction appelée getPhotos() à l'interface MarsApiService pour obtenir la chaîne de réponse du service Web.
interface MarsApiService {
    fun getPhotos()
}
  1. Utilisez l'annotation @GET pour indiquer à Retrofit qu'il s'agit d'une requête GET et spécifier un point de terminaison pour cette méthode de service Web. Dans ce cas, le point de terminaison est appelé photos. Comme indiqué dans la tâche précédente, vous utiliserez le point de terminaison /photos dans cet atelier de programmation.
import retrofit2.http.GET

interface MarsApiService {
    @GET("photos")
    fun getPhotos()
}

Lorsque la méthode getPhotos() est appelée, Retrofit ajoute le point de terminaison photos à l'URL de base (que vous avez définie dans le compilateur Retrofit) pour lancer la requête.

  1. Ajoutez un type renvoyé de la fonction à String.
interface MarsApiService {
    @GET("photos")
    fun getPhotos(): String
}

Déclarations d'objets

En Kotlin, les déclarations d'objets servent à déclarer des objets singleton. Le schéma singleton garantit qu'une seule et unique instance d'un objet est créée et qu'elle possède un point d'accès global à cet objet. L'initialisation de l'objet est thread-safe et effectuée lors du premier accès.

Vous trouverez ci-dessous un exemple de déclaration d'objet et de son accès. Le nom de la déclaration d'objet est toujours suivi du mot clé object (objet).

Exemple :

// Example for Object declaration, do not copy over

object SampleDataProvider {
    fun register(provider: SampleProvider) {
        // ...
    }
​
    // ...
}

// To refer to the object, use its name directly.
SampleDataProvider.register(...)

L'appel de la fonction create() sur un objet Retrofit est coûteux en termes de mémoire, de vitesse et de performances. L'application a besoin d'une seule instance du service de l'API Retrofit. Vous devez donc exposer le service au reste de l'application à l'aide de la déclaration d'objet.

  1. En dehors de la déclaration d'interface de MarsApiService, définissez un objet public appelé MarsApi pour initialiser le service Retrofit. Cet objet est l'objet singleton public auquel le reste de l'application peut accéder.
object MarsApi {}
  1. Dans la déclaration d'objet MarsApi, ajoutez une propriété d'objet initialisée en différé retrofitService de type MarsApiService. Vous effectuez cette initialisation différée, pour vous assurer qu'elle est initialisée lors de sa première utilisation. Ignorez l'erreur, que vous allez résoudre à l'étape suivante.
object MarsApi {
    val retrofitService : MarsApiService by lazy {}
}
  1. Initialisez la variable retrofitService à l'aide de la méthode retrofit.create() avec l'interface MarsApiService.
object MarsApi {
    val retrofitService : MarsApiService by lazy {
       retrofit.create(MarsApiService::class.java)
    }
}

La configuration de l'outil Retrofit est terminée. Chaque fois que votre application appelle MarsApi.retrofitService, l'appelant accède au même objet singleton Retrofit qui implémente MarsApiService, créé au premier accès. Dans la tâche suivante, vous utilisez l'objet Retrofit que vous avez implémenté.

Appeler le service Web dans MarsViewModel

Au cours de cette étape, vous implémentez la méthode getMarsPhotos() qui appelle le service REST, puis gère la chaîne JSON renvoyée.

ViewModelScope

Le viewModelScope est le champ d'application de la coroutine intégrée qui est défini pour chaque ViewModel dans votre application. Toute coroutine lancée dans ce champ d'application est automatiquement annulée si le ViewModel est effacé.

Vous pouvez utiliser viewModelScope pour lancer la coroutine et effectuer la requête de service Web en arrière-plan. Comme viewModelScope fait partie de ViewModel, la requête se poursuit même si l'application subit un changement de configuration.

  1. Dans le fichier MarsApiService.kt, définissez getPhotos() en tant que fonction de suspension pour la rendre asynchrone et ne pas bloquer le thread d'appel. Vous appelez cette fonction depuis un viewModelScope.
@GET("photos")
suspend fun getPhotos(): String
  1. Ouvrez le fichier ui/screens/MarsViewModel.kt. Faites défiler la page jusqu'à la méthode getMarsPhotos(). Supprimez la ligne qui définit la réponse d'état sur "Set the Mars API Response here!" afin que la méthode getMarsPhotos() soit vide.
private fun getMarsPhotos() {}
  1. Dans getMarsPhotos(), lancez la coroutine à l'aide de viewModelScope.launch.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

private fun getMarsPhotos() {
    viewModelScope.launch {}
}
  1. Dans viewModelScope, utilisez l'objet singleton MarsApi pour appeler la méthode getPhotos() à partir de l'interface retrofitService. Enregistrez la réponse renvoyée dans un val appelé listResult.
import com.example.marsphotos.network.MarsApi

viewModelScope.launch {
    val listResult = MarsApi.retrofitService.getPhotos()
}
  1. Attribuez le résultat que nous venons de recevoir du serveur backend à marsUiState. marsUiState est un objet d'état modifiable qui représente l'état de la requête Web la plus récente.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
  1. Exécutez l'application. Notez qu'elle se ferme immédiatement et qu'un message d'erreur peut s'afficher ou non. Il s'agit d'un plantage de l'application.
  2. Cliquez sur l'onglet Logcat dans Android Studio et examinez l'erreur dans le journal, qui commence par une ligne semblable à celle-ci : "------- beginning of crash".
    --------- beginning of crash
22803-22865/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
    Process: com.example.android.marsphotos, PID: 22803
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)
...

Ce message d'erreur indique que l'application ne dispose peut-être pas des autorisations INTERNET. La tâche suivante explique comment ajouter des autorisations Internet à l'application et résoudre ce problème.

7. Ajouter l'autorisation Internet et la gestion des exceptions

Autorisations Android

L'objectif des autorisations sur Android est de protéger la vie privée d'un utilisateur Android. Les applications Android doivent déclarer ou demander des autorisations pour accéder aux données utilisateur sensibles telles que les contacts, les journaux d'appels et certaines fonctionnalités du système, comme l'appareil photo ou Internet.

Pour que votre application accède à Internet, l'autorisation INTERNET est requise. La connexion à Internet pose un problème de sécurité, c'est pourquoi les applications n'ont pas de connexion Internet par défaut. Vous devez déclarer explicitement que l'application a besoin d'accéder à Internet. Cette autorisation est considérée comme normale. Pour en savoir plus sur les autorisations sur Android et leurs types, consultez la section Autorisations sur Android.

À cette étape, votre application déclare la ou les autorisations requises en incluant des balises <uses-permission> dans le fichier AndroidManifest.xml.

  1. Ouvrez manifests/AndroidManifest.xml. Ajoutez cette ligne juste avant la balise <application> :
<uses-permission android:name="android.permission.INTERNET" />
  1. Compilez et exécutez à nouveau l'application.

Si votre connexion Internet fonctionne, vous voyez le texte JSON contenant les données des photos de Mars. Vous remarquerez que les champs id et img_src sont répétés pour chaque enregistrement d'image. Vous découvrirez plus en détail le format JSON dans la suite de cet atelier de programmation.

b82ddb79eff61995.png

  1. Appuyez sur le bouton Retour de votre appareil ou de votre émulateur pour fermer l'application.

Gestion des exceptions

Votre code contient un bug. Pour le voir, procédez comme suit :

  1. Activez le mode Avion de votre appareil ou émulateur pour simuler une erreur de connexion réseau.
  2. Rouvrez l'application depuis le menu "Recents" (Applications récentes) ou redémarrez-la depuis Android Studio.
  3. Cliquez sur l'onglet Logcat dans Android Studio et notez l'exception fatale dans le journal :
3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.android.marsphotos, PID: 3302

Ce message d'erreur indique que l'application a essayé de se connecter et que le délai a expiré. De telles exceptions sont très courantes en temps réel. Contrairement au problème d'autorisation, vous ne pouvez pas résoudre cette erreur, mais vous pouvez la gérer. À l'étape suivante, vous découvrirez comment gérer ces exceptions.

Exceptions

Les exceptions sont des erreurs qui peuvent se produire pendant l'exécution (et non au moment de la compilation), et entraîner l'arrêt brutal de l'application sans en avertir l'utilisateur. Les exceptions peuvent nuire à l'expérience utilisateur. La gestion des exceptions est un mécanisme qui vous permet d'empêcher l'application de s'arrêter brusquement et de gérer la situation facilement.

Le motif des exceptions peut être une division par zéro ou une erreur de connexion réseau. Ces exceptions sont semblables à l'exception IllegalArgumentException déjà évoquée dans un atelier de programmation précédent.

Exemples de problèmes potentiels liés à la connexion à un serveur :

  • L'URL ou l'URI utilisés dans l'API sont incorrects.
  • Serveur indisponible, ce qui empêche l'application de s'y connecter.
  • Problème de latence du réseau.
  • Connexion Internet de mauvaise qualité ou inexistante sur l'appareil.

Ces exceptions ne peuvent pas être gérées lors de la compilation, mais vous pouvez utiliser un bloc try-catch pour les gérer lors de l'exécution. Pour en savoir plus, consultez Exceptions.

Exemple de syntaxe pour un bloc try-catch

try {
    // some code that can cause an exception.
}
catch (e: SomeException) {
    // handle the exception to avoid abrupt termination.
}

Dans le bloc try, ajoutez le code pour lequel vous prévoyez une exception. Dans votre application, il s'agit d'un appel réseau. Dans le bloc catch, vous devez implémenter le code qui empêche l'arrêt brutal de l'application. En cas d'exception, le bloc catch s'exécute pour récupérer de l'erreur au lieu d'arrêter brutalement l'application.

  1. Dans getMarsPhotos(), à l'intérieur du bloc launch, ajoutez un bloc try autour de l'appel MarsApi pour gérer les exceptions.
  2. Ajoutez un bloc catch après le bloc try.
import java.io.IOException

viewModelScope.launch {
   try {
       val listResult = MarsApi.retrofitService.getPhotos()
       marsUiState = listResult
   } catch (e: IOException) {

   }
}
  1. Exécutez l'application une fois de plus. Notez que cette fois-ci, l'application ne plante pas.

Ajouter une UI d'état

Dans la classe MarsViewModel, l'état de la requête Web la plus récente, marsUiState, est enregistré en tant qu'objet d'état modifiable. Toutefois, cette classe ne peut pas enregistrer un autre état (chargement, réussite et échec).

  • L'état Chargement indique que l'application attend des données.
  • L'état Réussite indique que les données ont bien été extraites du service Web.
  • L'état Erreur indique des erreurs de réseau ou de connexion.

Pour représenter ces trois états dans votre application, vous utilisez une interface scellée. Une sealed interface facilite la gestion de l'état en limitant les valeurs possibles. Dans l'application Mars Photos, vous limitez la réponse Web marsUiState à trois états (objets de classe de données) : chargement, réussite et erreur. Elle se présente comme suit :

// No need to copy over
sealed interface MarsUiState {
   data class Success : MarsUiState
   data class Loading : MarsUiState
   data class Error : MarsUiState
}

Dans l'extrait de code ci-dessus, en cas de réponse réussie, le serveur vous envoie des informations sur les photos de Mars. Pour stocker les données, ajoutez un paramètre constructeur à la classe de données Success.

Dans le cas des états Loading et Error, vous n'avez pas besoin de définir de nouvelles données ni de créer des objets : transmettez simplement la réponse Web. Remplacez la classe data par Object pour créer les objets des réponses Web.

  1. Ouvrez le fichier ui/MarsViewModel.kt. Après les instructions d'importation, ajoutez l'interface scellée MarsUiState. Cet ajout rend les valeurs de l'objet MarsUiState exhaustives.
sealed interface MarsUiState {
    data class Success(val photos: String) : MarsUiState
    object Error : MarsUiState
    object Loading : MarsUiState
}
  1. Dans la classe MarsViewModel, mettez à jour la définition marsUiState. Définissez le type sur MarsUiState et configurez MarsUiState.Loading comme valeur par défaut. Rendez le setter privé pour protéger marsUiState en écriture.
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
  private set
  1. Faites défiler la page jusqu'à la méthode getMarsPhotos(). Remplacez la valeur marsUiState par MarsUiState.Success, puis transmettez listResult.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
  1. Dans le bloc catch, gérez la réponse de l'échec. Définissez MarsUiState sur Error.
catch (e: IOException) {
   marsUiState = MarsUiState.Error
}
  1. Vous pouvez retirer l'attribution marsUiState du bloc try-catch. Votre fonction terminée doit se présenter comme suit :
private fun getMarsPhotos() {
   viewModelScope.launch {
       marsUiState = try {
           val listResult = MarsApi.retrofitService.getPhotos()
           MarsUiState.Success(listResult)
       } catch (e: IOException) {
           MarsUiState.Error
       }
   }
}
  1. Dans le fichier screens/HomeScreen.kt, ajoutez une expression when à marsUiState. Si marsUiState est défini sur MarsUiState.Success, appelez ResultScreen et transmettez marsUiState.photos. Ignorez les erreurs pour l'instant.
import androidx.compose.foundation.layout.fillMaxWidth

fun HomeScreen(
   marsUiState: MarsUiState,
   modifier: Modifier = Modifier
) {
    when (marsUiState) {
        is MarsUiState.Success -> ResultScreen(
            marsUiState.photos, modifier = modifier.fillMaxWidth()
        )
    }
}
  1. Dans le bloc when, ajoutez des vérifications pour MarsUiState.Loading et MarsUiState.Error. Demandez à l'application d'afficher les composables LoadingScreen, ResultScreen et ErrorScreen, que vous implémenterez ultérieurement.
import androidx.compose.foundation.layout.fillMaxSize

fun HomeScreen(
   marsUiState: MarsUiState,
   modifier: Modifier = Modifier
) {
    when (marsUiState) {
        is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
        is MarsUiState.Success -> ResultScreen(
            marsUiState.photos, modifier = modifier.fillMaxWidth()
        )

        is MarsUiState.Error -> ErrorScreen( modifier = modifier.fillMaxSize())
    }
}
  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.)

92a448fa23b6d1df.png

  1. Dans le fichier screens/HomeScreen.kt, sous le composable HomeScreen, ajoutez la fonction modulable LoadingScreen suivante pour afficher l'animation de chargement. La ressource drawable loading_img est incluse dans le code de démarrage.
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.Image

@Composable
fun LoadingScreen(modifier: Modifier = Modifier) {
    Image(
        modifier = modifier.size(200.dp),
        painter = painterResource(R.drawable.loading_img),
        contentDescription = stringResource(R.string.loading)
    )
}
  1. Sous le composable LoadingScreen, ajoutez la fonction modulable ErrorScreen suivante pour que l'application puisse afficher le message d'erreur.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding

@Composable
fun ErrorScreen(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier,
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = painterResource(id = R.drawable.ic_connection_error), contentDescription = ""
        )
        Text(text = stringResource(R.string.loading_failed), modifier = Modifier.padding(16.dp))
    }
}
  1. Exécutez à nouveau l'application, avec le mode Avion activé. Cette fois-ci, l'application ne se ferme pas brusquement et affiche le message d'erreur suivant :

28ba37928e0a9334.png

  1. Désactivez le mode Avion sur votre téléphone ou dans votre émulateur. Exécutez et testez votre application pour vérifier que tout fonctionne correctement et que vous pouvez voir la chaîne JSON.

8. Analyser la réponse JSON avec kotlinx.serialization

JSON

Les données demandées sont généralement dans un format de données courant comme XML ou JSON. Chaque appel renvoie des données structurées, et votre application doit connaître cette structure pour lire les données de la réponse.

Par exemple, dans cette application, vous récupérez les données du serveur https://android-kotlin-fun-mars-server.appspot.com/photos. En saisissant cette URL dans le navigateur, vous affichez une liste d'ID et d'URL d'images de la surface de Mars au format JSON.

Structure d'un exemple de réponse JSON

Affichage des paires clé-valeur et de l'objet JSON

La structure d'une réponse JSON présente les caractéristiques suivantes :

  • La réponse JSON est un tableau, indiqué entre crochets. Le tableau contient des objets JSON.
  • Les objets JSON sont entourés d'accolades.
  • Chaque objet JSON contient un ensemble de paires clé/valeur séparées par une virgule.
  • Dans chaque paire, la clé et la valeur sont séparées par un signe deux-points.
  • Les noms sont entourés de guillemets.
  • Les valeurs peuvent être des nombres, des chaînes, des valeurs booléennes, un tableau, un objet (objet JSON) ou la valeur "null".

Par exemple, img_src est une URL, c'est-à-dire une chaîne. Si vous collez l'URL dans un navigateur Web, une image de la surface de Mars s'affiche.

b4f9f196c64f02c3.png

Dans votre application, vous recevez maintenant une réponse JSON du service Web Mars, ce qui est un bon début. Mais, en réalité, vous avez besoin d'objets Kotlin pour afficher les images, et non d'une longue chaîne JSON. Ce processus est appelé désérialisation.

La sérialisation consiste à convertir les données utilisées par une application dans un format pouvant être transféré sur un réseau. Contrairement à la sérialisation, la désérialisation consiste à lire des données à partir d'une source externe (un serveur, par exemple) et à les convertir en objet d'exécution. Ce sont deux composants essentiels de la plupart des applications qui échangent des données sur le réseau.

kotlinx.serialization fournit des ensembles de bibliothèques qui convertissent une chaîne JSON en objets Kotlin. Une bibliothèque tierce développée par la communauté et compatible avec Retrofit est disponible : Kotlin Serialization Converter.

Dans cette tâche, vous allez utiliser la bibliothèque kotlinx.serialization pour analyser la réponse JSON du service Web dans des objets Kotlin utiles qui représentent des photos de Mars. Vous allez modifier l'application de sorte qu'au lieu d'afficher le fichier JSON brut, l'application affiche le nombre de photos de Mars renvoyées.

Ajouter des dépendances de bibliothèque kotlinx.serialization

  1. Ouvrez build.gradle.kts (Module :app).
  2. Dans le bloc plugins, ajoutez le plug-in kotlinx serialization.
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
  1. Dans la section dependencies, ajoutez le code suivant pour inclure la dépendance kotlinx.serialization. Cette dépendance fournit une sérialisation JSON pour les projets Kotlin.
// Kotlin serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
  1. Repérez les lignes du convertisseur scalaire Retrofit dans le bloc dependencies et modifiez-les pour utiliser kotlinx-serialization-converter :

Remplacer le code suivant

// Retrofit with scalar Converter
implementation("com.squareup.retrofit2:converter-scalars:2.9.0")

par le code suivant

// Retrofit with Kotlin serialization Converter

implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
  1. Cliquez sur Synchroniser pour créer à nouveau le projet avec les nouvelles dépendances.

Implémenter la classe de données Mars Photo

Un échantillon d'entrée de réponse JSON provenant du service Web ressemble à ceci (cela ressemble beaucoup à ce que nous avons vu auparavant) :

[
    {
        "id":"424906",
        "img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
    },
...]

Dans l'exemple ci-dessus, notez que chaque entrée de photo sur Mars comporte les paires clé/valeur JSON suivantes :

  • id : ID de la propriété, sous forme de chaîne. Étant donné qu'il est entre guillemets " ", il est de type String et non Integer.
  • img_src : URL de l'image sous forme de chaîne.

kotlinx.serialization analyse ces données JSON et les convertit en objets Kotlin. Pour ce faire, kotlinx.serialization doit disposer d'une classe de données Kotlin pour stocker les résultats analysés. Dans cette étape, vous allez créer la classe de données MarsPhoto.

  1. Effectuez un clic droit sur le package network, puis sélectionnez New > Kotlin File/Class (Nouveau > Fichier/Classe Kotlin).
  2. Dans la boîte de dialogue, sélectionnez Class (Classe), puis saisissez MarsPhoto comme nom de classe. Cette opération crée un fichier nommé MarsPhoto.kt dans le package network.
  3. Transformez MarsPhoto en classe de données en ajoutant le mot clé data avant la définition de classe.
  4. Remplacez les accolades {} par des parenthèses (). Cette modification génère une erreur, car au moins une propriété doit être définie pour les classes de données.
data class MarsPhoto()
  1. Ajoutez les propriétés suivantes à la définition de la classe MarsPhoto.
data class MarsPhoto(
    val id: String,  val img_src: String
)
  1. Pour rendre une classe MarsPhoto sérialisable, annotez-la avec @Serializable.
import kotlinx.serialization.Serializable

@Serializable
data class MarsPhoto(
    val id: String,  val img_src: String
)

Notez que chacune des variables de la classe MarsPhoto correspond à un nom de clé dans l'objet JSON. Pour faire correspondre les types à notre réponse JSON, utilisez des objets String pour toutes les valeurs.

Lorsque kotlinx serialization analyse le fichier JSON, il met en correspondance les clés en fonction des noms et remplit les objets de données avec les bonnes valeurs.

Annotation @SerialName

Dans les réponses JSON, les noms de clés peuvent parfois rendre les propriétés Kotlin déroutantes, ou ne pas correspondre au style de codage recommandé. Par exemple, dans le fichier JSON, la clé img_src utilise un underscore (ou tiret du bas), tandis que la convention Kotlin pour les propriétés utilise des lettres majuscules et minuscules ("camel case").

Pour utiliser dans votre classe de données des noms de variables différents des noms de clés de la réponse JSON, utilisez l'annotation @SerialName. Dans l'exemple suivant, le nom de la variable dans la classe de données est imgSrc. La variable peut être mappée à l'attribut JSON img_src à l'aide de @SerialName(value = "img_src").

  1. Remplacez la ligne de la clé img_src par celle indiquée ci-dessous.
import kotlinx.serialization.SerialName

@SerialName(value = "img_src")
val imgSrc: String

Mettre à jour MarsApiService et MarsViewModel

Dans cette tâche, vous allez utiliser le convertisseur kotlinx.serialization pour convertir l'objet JSON en objets Kotlin.

  1. Ouvrez network/MarsApiService.kt.
  2. Remarquez les erreurs de référence non résolues pour ScalarsConverterFactory. Ces erreurs sont dues à la modification de la dépendance Retrofit mentionnée dans une section précédente.
  3. Supprimez l'importation pour ScalarConverterFactory. Vous corrigerez l'autre erreur ultérieurement.

Suppression :

import retrofit2.converter.scalars.ScalarsConverterFactory
  1. Dans la déclaration d'objet retrofit, modifiez le compilateur Refit pour qu'il utilise kotlinx.serialization au lieu de ScalarConverterFactory.
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json
import okhttp3.MediaType

private val retrofit = Retrofit.Builder()
        .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
        .baseUrl(BASE_URL)
        .build()

Maintenant que kotlinx.serialization est en place, vous pouvez demander à Retrofit de renvoyer une liste d'objets MarsPhoto depuis le tableau JSON au lieu de renvoyer une chaîne JSON.

  1. Mettez à jour l'interface MarsApiService pour Retrofit afin de renvoyer une liste d'objets MarsPhoto au lieu de renvoyer une String.
interface MarsApiService {
    @GET("photos")
    suspend fun getPhotos(): List<MarsPhoto>
}
  1. Effectuez des modifications similaires dans viewModel. Ouvrez MarsViewModel.kt et faites défiler la page jusqu'à la méthode getMarsPhotos().

Dans la méthode getMarsPhotos(), listResult est un List<MarsPhoto> et non plus une String. La taille de cette liste correspond au nombre de photos reçues et analysées.

  1. Pour afficher le nombre de photos récupérées, mettez à jour marsUiState comme suit :
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(
   "Success: ${listResult.size} Mars photos retrieved"
)
  1. Assurez-vous que le mode Avion est désactivé sur votre appareil ou dans votre émulateur. Compilez et exécutez l'application.

Cette fois, le message devrait indiquer le nombre de propriétés renvoyées par le service Web (et non pas une longue chaîne JSON) :

a59e55909b6e9213.png

9. Code de solution

Pour télécharger le code de cet atelier de programmation terminé, utilisez les commandes Git suivantes :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout repo-starter

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous le souhaitez, vous pouvez consulter le code de solution de cet atelier de programmation sur GitHub.

10. Résumé

Services Web REST

  • Un service Web est une fonctionnalité logicielle sur Internet qui permet à votre application d'effectuer des requêtes et d'obtenir des données.
  • Les services Web courants utilisent une architecture REST. Les services Web qui proposent une architecture REST sont appelés services RESTful. Les services Web RESTful sont conçus à l'aide de protocoles et de composants Web standards.
  • Vous envoyez une requête à un service Web REST de manière standardisée, via des URI.
  • Pour utiliser un service Web, une application doit établir une connexion réseau et communiquer avec le service. L'application doit ensuite recevoir et analyser les données de réponse dans un format qu'elle peut utiliser.
  • La bibliothèque Retrofit est une bibliothèque cliente qui permet à votre application d'envoyer des requêtes à un service Web REST.
  • Utilisez des convertisseurs pour indiquer à Retrofit comment traiter les données qu'il envoie au service Web et qu'il récupère. Par exemple, ScalarsConverter traite les données du service Web comme une String ou tout autre type primitif.
  • Pour autoriser votre application à établir une connexion à Internet, ajoutez l'autorisation "android.permission.INTERNET" dans le fichier manifeste Android.
  • L'initialisation différée délègue la création d'un objet à sa première utilisation. La référence est créée, mais pas l'objet lui-même. Lors du premier accès à un objet, une référence est créée. Elle est réutilisée ensuite à chaque fois.

Analyse JSON

  • La réponse d'un service Web est souvent au format JSON, couramment utilisé pour représenter des données structurées.
  • Un objet JSON est une collection de paires clé-valeur.
  • Une collection d'objets JSON est un tableau JSON. Vous obtenez un tableau JSON en tant que réponse d'un service Web.
  • Les clés d'une paire clé-valeur sont comprises entre guillemets. Les valeurs peuvent être des nombres ou des chaînes.
  • En Kotlin, les outils de sérialisation des données sont disponibles dans un composant distinct, kotlinx.serialization. kotlinx.Serialization fournit des ensembles de bibliothèques qui convertissent une chaîne JSON en objets Kotlin.
  • Une bibliothèque Kotlin développée par la communauté est disponible pour Retrofit : retrofit2-kotlinx-serialization-converter.
  • kotlinx.serialization met en correspondance les clés d'une réponse JSON avec les propriétés d'un objet de données portant le même nom.
  • Pour utiliser un nom de propriété différent pour une clé, annotez cette propriété avec @SerialName et la clé JSON value.

11. En savoir plus

Documentation pour les développeurs Android :

Documentation Kotlin :

Autre :