Dans un monde idéal, une application mobile travaillera avec des données locales afin de pouvoir fonctionner même si elle n'a pas accès à Internet.
De plus, elle synchronisera ces données avec une base de données distante dès qu'elle aura accès à Internet afin d'assurer de ne rien perdre en cas de bris.
Vous pouvez cependant développer une application qui n'utilise qu'une base de données distante si votre application répond à l'une de ces conditions :
ou
ou
Vous désirez coder une telle application? Suivez ces étapes!
Dans cette fiche :
Pour permettre l'utilisation d'un API qui permettra d'accéder aux données distantes, il faut ajouter des dépendances à votre projet.
Ces lignes doivent être ajoutées dans le fichier build.gradle.kts qui se trouve dans le dossier app.
...
dependencies {
...
// Pour appel API
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.retrofit2:converter-gson:3.0.0")
}
Une fois les dépendances ajoutées, il faut resynchroniser le projet pour qu'il tienne compte de l'ajout.
Note : si vous obtenez un message du genre « Unresolved reference retrofit2 », rendez-vous dans le menu / .
Pour qu'une application Android puisse utiliser une ressource en ligne, il faut ajouter une balise uses-permission dans le fichier AndroidManifest.xml que l'on retrouve dans le dossier app/src/main.
Sans cette permission, le programme plantera avec le message « Permission denied (missing INTERNET permission?) ».
Remarquez que l'usager n'aura pas à donner son accord avant d'accéder à Internet. La permission est une simple déclaration dans le manifeste.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
...
</application>
</manifest>
De plus, si vous travaillez avec un URL non sécurisé (http://) pendant le développement, vous devrez le préciser comme suit.
Important : il faut remettre cette configuration à false lorsque l'application sera en production.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
...
<application
android:usesCleartextTraffic="true"
...
</application>
</manifest>
Lorsque l'application fera un appel à l'API, elle stockera les données reçues dans une instance d'une classe spécialisée.
Cette classe, qui est une classe de données, sera placée dans un dossier nommé data qui est au même niveau que le fichier MainActiviy.kt, par exemple app/src/main/java/com/monnom/monprojet/data/Item.kt.
Pour créer ce dossier dans Android Studio : Clic droit sur son dossier parent / / .
La classe doit avoir une propriété pour chacune des informations reçues. Le nom d'une propriété doit correspondre à une clé JSON reçue.
Par exemple, si l'API retourne un item dont les données sont au format : {"id": 3, "titre": "abc"}, vous devez déclarer une classe avec les propriétés suivantes :
data class Item(
var id: Int?, // optionnel car on ne le spécifiera pas lors de l'ajout d'un item
var titre: String,
)
La conversion des données JSON retournées par l'API en objets Kotlin utilisés par l'application sera automatisée grâce à l'instruction addConverterFactory(GsonConverterFactory.create()) dans l'instance Retrofit que nous créerons plus bas.
Notez que si l'API retourne des informations dont la clé n'a pas de propriété correspondante, ces informations ne seront simplement pas traitées par l'application.
Inversement, si la classe contient des propriétés qui ne sont pas retournées par l'API, ces propriétés auront toujours la valeur null.
Mais attention : si vous devez envoyer des données de ce type dans le corps de la requête, par exemple pour ajouter une donnée, une erreur dans le nom des champs des données attendues par l'API générera une erreur 404 (ou 500 selon la façon dont l'API a été programmée).
Il faut créer une interface qui fera le lien entre l'application et l'API.
Cette interface sera codée dans un fichier dont le nom se termine par Api, placé dans le dossier service qui est au même niveau que le fichier MainActiviy.kt, par exemple app/src/main/java/com/monnom/monprojet/service/ItemApi.kt.
L'interface doit définir, pour chaque type de requête à réaliser :
Par exemple, si on appelle l'API https://monapi.com/v1/items, le point d'accès est items. Avec l'API https://monapi.com/v1/ajouter.php, le point d'accès est ajouter.php.
Pour un URL qui contient des paramètres dans son chemin, par exemple https://monapi.com/v1/items/12, le point d'accès est items/{id}. Les paramètres du chemin seront identifiés à l'aide de l'annotation @Path (voir exemple plus bas).
Dans le cas où l'URL utilise des paramètres de requête, par exemple https://monapi.com/v1/items?id=12, le point d'accès est items. Les paramètres de requête seront identifiés à l'aide de l'annotation @Query (voir exemple plus bas).
Notez que si le type de retour est Response<...>, il sera possible de retrouver le code d'état HTTP retourné par l'API.
Voici un exemple d'interface qui définit quelques appels. Le premier permet de retrouver tous les items et le second, un seul item retrouvé par son identifiant, passé comme paramètre de requête (ex : ?id=12).
Le nom du paramètre n'a pas d'importance. Je l'ai appelé identifiant pour illustrer que ça n'a pas besoin d'être le même nom que dans l'API. Sa valeur sera spécifiée lors de l'appel à cette fonction (voir correspondance de couleur plus bas).
Un troisième appel permet d'ajouter un item dans la base de données distante.
interface ItemApi {
@GET("liste")
suspend fun retrouverItems(): Response<List<Item>>
@GET("liste")
suspend fun retrouverUnItem(@Query("id") identifiant: Int): Response<Item>
@POST("ajout")
suspend fun ajouterItem(@Body item: Item): Response<Void>
}
Si on avait utilisé un API qui utilise des paramètres de chemin, la seconde requête aurait pris cette forme :
@GET("liste/{id}")
suspend fun retrouverUnItem(@Path("id") identifiant: Int): Response<Item>
Note : si vous effectuez des recherches sur le Web ou dans des anciens projets, vous rencontrerez parfois des instructions du genre :
fun retrouverUnItem(@Path("id") identifiant: Int): Call<Item>.
Ce type de code était utilisé avant l'arrivée des coroutine de Kotlin. Bien qu'il fonctionne encore, il est préférable d'utiliser l'approche avec coroutines (avec le mot-clé suspend, tel qu'illustré plus haut) puisque le code sera plus facile à écrire, à lire et à maintenir.
Dans le cadre de ce cours, la forme avec Call est interdite.
L'application travaillera avec une seule instance de Retrofit.
L'instanciation sera codée dans le fichier service/RetrofitInstance.kt.
On utilisera le mot-clé object et non class. En Kotlin, le mot-clé object permet de déclarer une classe et d'instancier un singleton de cette classe, tout ça en une seule étape.
Il s'agit de spécifier l'URL de base, d'effectuer l'instanciation en tant que telle et de faire le lien avec l'interface (fichier créé plus tôt, dont le nom se termine par Api).
object RetrofitInstance {
private const val BASE_URL = "https://monapi.com/v1/"
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val itemApi: ItemApi by lazy {
retrofit.create(ItemApi::class.java)
}
}
Dans cet extrait de code, l'instruction addConverterFactory(GsonConverterFactory.create()) permet d'automatiser la conversion de données JSON en objets Kotlin et vice versa.
Il est maintenant temps de coder les fonctions qui permettent d'appeler l'API. Ces fonctions, qui constituent la logique métier, seront codées dans le ViewModel.
Je leur ai volontairement donné des noms différents de ceux spécifiés dans l'interface afin de mieux illustrer qu'est-ce qui fait quoi.
Chacune de ces fonctions fera appel à la fonction dont le nom a été spécifié dans l'interface. Une fois l'appel réalisé, elle stockera dans une variable d'état les informations retournées par l'API.
Remarquez que dans cet extrait, il n'est pas utile de déclarer le uiState comme un flux puisque l'application n'écoute pas pour recevoir les modifications aux données distantes.
J'ai mis en caractères gras le code qui diffère lorsque le uiState n'est pas un flux.
class HomeViewModel : ViewModel() {
var uiState = mutableStateOf(HomeUiState())
private set
init {
rechercherItems()
}
Un composable pourra alors appeler une méthode du ViewModel pour réaliser l'appel.
@Composable
fun AfficherItem(viewModel: HomeViewModel, id: Int) {
Button(
onClick = {
viewModel.rechercherUnItem(id)
}
) {
Text(text = "Rechercher")
}
...
}
Une fois l'appel à l'API complété (et les informations stockées dans le ViewModel), un composable pourra afficher les données du ViewModel.
Remarquez la syntaxe pour initialiser le uiState lorsqu'on ne travaille pas avec un flux.
Ici aussi, j'ai mis en caractères gras le code qui diffère quand le uiState n'est pas un flux.
@Composable
fun UnItem(viewModel: HomeViewModel) {
val uiState by viewModel.uiState
...
Text(text = uiState.unItem?.id?.toString() ?: "---")
...
}
Prenons le cas d'un API qui retourne un tableau d'items quand la requête fonctionne ou un message d'erreur si un problème survient.
Ce message d'erreur, initialisé par l'API, est plus précis qu'un simple Not Found ou Internal Server Error.
Pour avoir accès à ce message, il faut d'abord créer une classe dont les propriétés correspondent aux clés JSON reçues.
data class ReponseAvecMessage (
var message: String? = null,
)
Dans le ViewModel, il sera possible de retrouver le message d'erreur retourné par l'API à l'aide manipulations JSON.
fun retrouverItems() {
viewModelScope.launch {
try {
val reponse = RetrofitInstance.itemApi.retrouverItems()
if (reponse.isSuccessful) {
...
}
else {
val gson = Gson()
val reponseAvecMessage: ReponseAvecMessage? = gson?.fromJson(reponse?.errorBody()?.charStream()?.readText(), ReponseAvecMessage::class.java)
val message = reponseAvecMessage?.message ?: "Aucun message"
...
}
} catch (e: Exception) {
...
}
}
}
L'erreur «Erreur « Unable to resolve host "....com": No address associated with hostname » indique qu'il y a un problème avec le serveur DNS qui doit traduire un URL en adresse IP.
Cette erreur est généralement silencieuse. Vous la verrez seulement si vous avez pris soin de réagir à un problème lors de l'appel de l'API.
try {
val reponse = RetrofitInstance.itemApi.retrouverItems()
...
} catch (e: Exception) {
Log.d("****** ViewModel", "Erreur lors de l'appel de retrouverItems() : ${e.message}")
...
}
Si vous voyez cette erreur, commencez par vérifier si l'URL est exact à l'aide d'un navigateur Web ou de Postman. Vous devez concaténer la valeur de la constante BASE_URL avec le point d'accès précisé à la suite du @GET ou du @POST (ex : https://monapi.com/v1/liste).
Si l'URL est exact, l'erreur pourrait être due à un problème avec l'émulateur. Ceci arrive parfois si on utilise l'émulateur dans différents réseaux, par exemple à l'école et à la maison.
Pour régler ce problème :
▼Publicité