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.
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
.
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
- Ouvrez le projet téléchargé dans Android Studio. Le nom du dossier du projet est
basic-android-kotlin-compose-training-mars-photos
. - Dans le volet Android, développez app > kotlin + java. Notez que l'application comporte un dossier de package nommé
ui
. Il s'agit de la couche de l'UI de l'application.
- 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.
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 composableHomeScreen
. 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éemarsUiState
. 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
duMutableState
à l'aide de données issues d'Internet.
screens\HomeScreen.kt
:
- Ce fichier contient les composables
HomeScreen
etResultScreen
.ResultScreen
présente une mise en pageBox
simple qui affiche la valeur demarsUiState
dans un composableText
.
MainActivity.kt
:
- La seule tâche de cette activité consiste à charger le
ViewModel
et à afficher le composableMarsPhotosApp
.
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.
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.
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.
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.
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.
- Ouvrez le fichier Gradle
build.gradle.kts (Module :app)
au niveau du module. - 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.
- 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.
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 :
- Effectuez un clic droit sur le package com.example.marsphotos dans le volet de votre projet Android, puis sélectionnez New > Package (Nouveau > Package).
- Dans la fenêtre pop-up, ajoutez network à la fin du nom de package suggéré.
- Créez un fichier Kotlin sous le nouveau package. Nommez-le
MarsApiService
. - Ouvrez
network/MarsApiService.kt
. - 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"
- 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.
- Appelez
addConverterFactory()
sur le compilateur avec une instance deScalarsConverterFactory
.
import retrofit2.converter.scalars.ScalarsConverterFactory
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
- Ajoutez l'URL de base du service Web à l'aide de la méthode
baseUrl()
. - Appelez
build()
pour créer l'objet Retrofit.
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
- 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 {
}
- Ajoutez une fonction appelée
getPhotos()
à l'interfaceMarsApiService
pour obtenir la chaîne de réponse du service Web.
interface MarsApiService {
fun getPhotos()
}
- 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.
- 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.
- 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 {}
- Dans la déclaration d'objet
MarsApi
, ajoutez une propriété d'objet initialisée en différéretrofitService
de typeMarsApiService
. 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 {}
}
- Initialisez la variable
retrofitService
à l'aide de la méthoderetrofit.create()
avec l'interfaceMarsApiService
.
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.
- Dans le fichier
MarsApiService.kt
, définissezgetPhotos()
en tant que fonction de suspension pour la rendre asynchrone et ne pas bloquer le thread d'appel. Vous appelez cette fonction depuis unviewModelScope
.
@GET("photos")
suspend fun getPhotos(): String
- Ouvrez le fichier
ui/screens/MarsViewModel.kt
. Faites défiler la page jusqu'à la méthodegetMarsPhotos()
. Supprimez la ligne qui définit la réponse d'état sur"Set the Mars API Response here!"
afin que la méthodegetMarsPhotos()
soit vide.
private fun getMarsPhotos() {}
- Dans
getMarsPhotos()
, lancez la coroutine à l'aide deviewModelScope.launch
.
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
private fun getMarsPhotos() {
viewModelScope.launch {}
}
- Dans
viewModelScope
, utilisez l'objet singletonMarsApi
pour appeler la méthodegetPhotos()
à partir de l'interfaceretrofitService
. Enregistrez la réponse renvoyée dans unval
appelélistResult
.
import com.example.marsphotos.network.MarsApi
viewModelScope.launch {
val listResult = MarsApi.retrofitService.getPhotos()
}
- 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
- 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.
- 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
.
- Ouvrez
manifests/AndroidManifest.xml
. Ajoutez cette ligne juste avant la balise<application>
:
<uses-permission android:name="android.permission.INTERNET" />
- 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.
- 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 :
- Activez le mode Avion de votre appareil ou émulateur pour simuler une erreur de connexion réseau.
- Rouvrez l'application depuis le menu "Recents" (Applications récentes) ou redémarrez-la depuis Android Studio.
- 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.
- Dans
getMarsPhotos()
, à l'intérieur du bloclaunch
, ajoutez un bloctry
autour de l'appelMarsApi
pour gérer les exceptions. - Ajoutez un bloc
catch
après le bloctry
.
import java.io.IOException
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = listResult
} catch (e: IOException) {
}
}
- 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.
- Ouvrez le fichier
ui/MarsViewModel.kt
. Après les instructions d'importation, ajoutez l'interface scelléeMarsUiState
. Cet ajout rend les valeurs de l'objetMarsUiState
exhaustives.
sealed interface MarsUiState {
data class Success(val photos: String) : MarsUiState
object Error : MarsUiState
object Loading : MarsUiState
}
- Dans la classe
MarsViewModel
, mettez à jour la définitionmarsUiState
. Définissez le type surMarsUiState
et configurezMarsUiState.Loading
comme valeur par défaut. Rendez le setter privé pour protégermarsUiState
en écriture.
var marsUiState: MarsUiState by mutableStateOf(MarsUiState.Loading)
private set
- Faites défiler la page jusqu'à la méthode
getMarsPhotos()
. Remplacez la valeurmarsUiState
parMarsUiState.Success
, puis transmettezlistResult
.
val listResult = MarsApi.retrofitService.getPhotos()
marsUiState = MarsUiState.Success(listResult)
- Dans le bloc
catch
, gérez la réponse de l'échec. DéfinissezMarsUiState
surError
.
catch (e: IOException) {
marsUiState = MarsUiState.Error
}
- Vous pouvez retirer l'attribution
marsUiState
du bloctry-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
}
}
}
- Dans le fichier
screens/HomeScreen.kt
, ajoutez une expressionwhen
àmarsUiState
. SimarsUiState
est défini surMarsUiState.Success
, appelezResultScreen
et transmettezmarsUiState.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()
)
}
}
- Dans le bloc
when
, ajoutez des vérifications pourMarsUiState.Loading
etMarsUiState.Error
. Demandez à l'application d'afficher les composablesLoadingScreen
,ResultScreen
etErrorScreen
, 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())
}
}
- 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.)
- Dans le fichier
screens/HomeScreen.kt
, sous le composableHomeScreen
, ajoutez la fonction modulableLoadingScreen
suivante pour afficher l'animation de chargement. La ressource drawableloading_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)
)
}
- Sous le composable
LoadingScreen
, ajoutez la fonction modulableErrorScreen
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))
}
}
- 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 :
- 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
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.
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
- Ouvrez
build.gradle.kts (Module :app)
. - Dans le bloc
plugins
, ajoutez le plug-inkotlinx serialization
.
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
- Dans la section
dependencies
, ajoutez le code suivant pour inclure la dépendancekotlinx.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")
- Repérez les lignes du convertisseur scalaire Retrofit dans le bloc
dependencies
et modifiez-les pour utiliserkotlinx-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")
- 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 typeString
et nonInteger
.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
.
- Effectuez un clic droit sur le package network, puis sélectionnez New > Kotlin File/Class (Nouveau > Fichier/Classe Kotlin).
- 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 packagenetwork
. - Transformez
MarsPhoto
en classe de données en ajoutant le mot clédata
avant la définition de classe. - 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()
- Ajoutez les propriétés suivantes à la définition de la classe
MarsPhoto
.
data class MarsPhoto(
val id: String, val img_src: String
)
- 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")
.
- 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.
- Ouvrez
network/MarsApiService.kt
. - 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. - Supprimez l'importation pour
ScalarConverterFactory
. Vous corrigerez l'autre erreur ultérieurement.
Suppression :
import retrofit2.converter.scalars.ScalarsConverterFactory
- Dans la déclaration d'objet
retrofit
, modifiez le compilateur Refit pour qu'il utilisekotlinx.serialization
au lieu deScalarConverterFactory
.
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.
- Mettez à jour l'interface
MarsApiService
pour Retrofit afin de renvoyer une liste d'objetsMarsPhoto
au lieu de renvoyer uneString
.
interface MarsApiService {
@GET("photos")
suspend fun getPhotos(): List<MarsPhoto>
}
- Effectuez des modifications similaires dans
viewModel
. OuvrezMarsViewModel.kt
et faites défiler la page jusqu'à la méthodegetMarsPhotos()
.
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.
- 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"
)
- 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) :
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 uneString
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é JSONvalue
.
11. En savoir plus
Documentation pour les développeurs Android :
- Guide sur l'architecture des applications | Développeurs Android
- Présentation de ViewModel
- ViewModelScope
Documentation Kotlin :
- Exceptions : try, catch, finally, throw, Nothing
- Coroutines, documentation officielle
- Répartiteurs et contextes de coroutines
- Sérialisation | Kotlin
Autre :