Accès Web¶
Le projet d’exemple pour ce chapitre
Vous pouvez télécharger le projet contenant les exemples de ce chapitre :
android-demo-app.zip
Il est courant pour une application mobile d’échanger des données par une interface réseau. Les périphériques Android offrent de nombreuses interface : Web avec le Wifi, USB, NFC, Bluetooth. Dans ce chapitre nous verrons comment échanger des données sur le Web en interagissant avec des API Web.
Permission pour l’accès Internet¶
Certaines fonctionnalités ne sont accessibles que si l’application dispose
des droits suffisants. Pour accéder à Internet, une application doit obtenir
la permission pour le droit android.permission.INTERNET
. Les permissions
requises sont listées dans le manifeste de l’application. Elles sont ensuite
présentées à l’utilisateur au moment de l’installation de l’application. Il doit
valider l’autorisation de ces permissions pour finaliser l’installation.
Note
Quand vous installez une application depuis Android Studio, vous n’avez pas besoin de valider les permissions de votre application.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.gayerie.monappli">
<uses-permission android:name="android.permission.INTERNET" />
<!--
...
-->
</manifest>
Utilisation de la classe URL¶
Pour accéder à une ressource sur le Web, vous pouvez utiliser les classes fournies directement par l’API Java. Cependant, il est nécessaire de prendre en compte les problématiques de développement asynchrone dans une application Android.
La classe URL en Java sert, bien évidemment, à représenter une URL. Mais elle
offre également une méthode openConnection qui permet d’initier une connexion à
l’adresse représentée. Cette méthode s’occupe de retourner un objet de type
URLConnection. Il est possible de transtyper (cast) cet objet selon le schéma de
connexion utilisé. Par, exemple, l’API Android assure qu’elle peut prendre en
charge le schéma http
avec la classe HttpURLConnection et le schéma https
avec la classe HttpsURLConnection. À partir de cette connexion, on peut obtenir
un flux d’entrée permettant de lire la réponse.
1 2 3 4 5 6 7 8 9 10 11 12 13 | Scanner scanner = null;
String response = "";
try {
URL url = new URL("http://monsite.fr/mapage");
URLConnection connection = url.openConnection();
InputStream stream = connection.getInputStream();
scanner = new Scanner(stream).useDelimiter("\0");
response = scanner.next();
} finally {
if (scanner != null) {
scanner.close();
}
}
|
Pour simplifier l’écriture, le code ci-dessus utilise un objet de type Scanner pour lire le flux d’entrée. Même s’il est fonctionnel, ce code reste simpliste et laisse de côté la gestion des erreurs, la gestion de l’encodage du message reçu, le positionnement d’en-têtes pour la requête HTTP… Mais surtout, si vous exécutez ce code dans une activité, vous obtiendrez une exception de type NetworkOnMainThreadException car Android refuse de réaliser des accès réseaux directement depuis le code d’une activité. Cela tient au fait qu’il est difficile d’anticiper le temps nécessaire au traitement d’une requête HTTP : elle peut être quasi-instantanée ou nécessiter plusieurs secondes, voire dizaines de secondes. Or ce code est bloquant, puisqu’à la ligne 8, l’appel à la méthode next ne retourne que lorsque la totalité de la réponse aura été lue. Cela signifie que l’activité serait figée et que l’utilisateur ne pourrait plus interagir avec l’application pendant ce temps. Afin d’éviter ce comportement, Android nous impose d’utiliser un appel asynchrone.
Les appels asynchrones permettent de réaliser des traitements pour lesquels nous ne souhaitons pas que le programme attende le résultat. Cela permet à l’application de ne pas se bloquer et de rester réactive aux demandes de l’utilisateur. Pour une application Android, les traitements pouvant prendre plusieurs secondes doivent être réalisés de manière asynchrone. Pour les appels réseaux, cela est rendu obligatoire par l’implémentation de l’API Android.
Un appel asynchrone peut être réalisé en utilisant les classes fournies par l’API Java telles que Thread ou l’interface Executor du package java.util.concurrent. Android fournit également la classe abstraite AsyncTask que nous allons utiliser comme exemple.
La classe abstraite AsyncTask est une classe générique, qui dépend du type de données en entrée, du type de données pour indiquer la progression de la tâche et du type de données produites par la tâche. Pour notre exemple, en entrée nous aurons des objets de type URL représentant les URL et en sortie une String pour le contenu des pages. L’indicateur de progression est généralement un Integer pour représenter un pourcentage de progression (pour garder notre exemple simple, nous n’utiliserons pas le système de progression). Nous devons fournir une implémentation de la méthode abstraite doInBackground qui représente le traitement de la tâche.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | package dev.gayerie.monappli;
import android.os.AsyncTask;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;
public class DownloadTextTask extends AsyncTask<URL, Integer, String> {
private static final String TAG = "DownloadTextTask";
@Override
protected String doInBackground(URL... urls) {
StringBuilder buffer = new StringBuilder();
for(URL url: urls) {
try {
buffer.append(download(url));
} catch (IOException e) {
Log.e(TAG, "Erreur lors de l'accès à Internet", e);
}
}
return buffer.toString();
}
private String download(URL url) throws IOException {
Scanner scanner = null;
try {
URLConnection connection = url.openConnection();
InputStream stream = connection.getInputStream();
scanner = new Scanner(stream).useDelimiter("\0");
return scanner.next();
} finally {
if (scanner != null) {
scanner.close();
}
}
}
}
|
La classe DownloadTextTask
implémente la méthode privée download
qui
reprend le code de téléchargement vu plus haut. Cette classe hérite de la méthode
execute qui permet de passer la tâche dans une file d’attente d’exécution.
Cette file d’attente est prise en charge par un autre thread qui appellera une
à une la méthode doInBackground de toutes les tâches. Ce mécanisme permet de
garantir que le code de la tâche ne sera pas exécuté dans le même thread que
celui de l’activité.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | package dev.gayerie.monappli;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import java.net.MalformedURLException;
import java.net.URL;
public class InternetActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_internet);
textView = findViewById(R.id.infoView);
}
@Override
protected void onResume() {
super.onResume();
DownloadTextTask downloadTextTask = new DownloadTextTask() {
@Override
protected void onPostExecute(String data) {
// ce code est appelé après le traitement asynchrone de doInBackground
textView.setText(data);
}
};
textView.setText("Chargement en cours...");
try {
URL url = new URL("https://tools.ietf.org/rfc/rfc7230.txt");
downloadTextTask.execute(url);
} catch (MalformedURLException e) {
textView.setText("L'URL n'est pas bonne");
}
}
}
|
L’activité InternetActivity
récupère le document à l’adresse
https://tools.ietf.org/rfc/rfc7230.txt pour l’afficher dans un composant TextView.
La méthode onCreate
se limite à mettre en place le contenu de la vue à
partir d’un layout et de récupérer le composant TextView qui contiendra le
document téléchargé.
La méthode onResume
réalise l’appel asynchrone. Elle définit une classe
anonyme qui hérite de DownloadTextTask
en redéfinissant la méthode onPostExecute
(lignes 23 à 29). La méthode onPostExecute est fournie par
la classe AsyncTask. Elle ne réalise aucun traitement par défaut. Elle est appelée
dès que l’appel à la méthode doInBackground est fini. AsyncTask nous garantit
que la méthode onPostExecute est appelée dans le thread de l’activité et qu’elle
reçoit en paramètre l’objet qui a été retourné par l’appel à doInBackground.
Nous pouvons donc récupérer le texte téléchargé et le positionner dans le
TextView (ligne 27). Les lignes 33 et 34 créent l’URL et la passe en paramètre
de la méthode execute (ligne 34) qui déclenche l’appel asynchrone.
On voit qu’une requête HTTP nécessite une attention particulière à cause de la prise en charge du traitement asynchrone. Si l’exemple précédent a la vertu de rendre apparents les mécanismes mis en œuvre pour réaliser ce traitement asynchrone, le code produit est complexe à comprendre et source d’erreur lorsqu’une application doit massivement faire des appels Web. Nous allons voir que nous pouvons beaucoup simplifier notre code en ayant recours à une bibliothèque tierce qui encapsulera une bonne partie de ces mécanismes pour nous.
Note
L’utilisation de AsyncTask est dépréciée pour les dernière versions de l’API Android. En effet, le code présenté ci-dessus n’est pas exempt de problèmes. À la ligne 27, on met à jour le TextView en résultat à l’appel à la tâche distante. Mais qu’est-ce qui nous garantit que ce TextView est encore disponible à ce moment-là ? Si la requête met plusieurs secondes pour s’exécuter, l’utilisateur peut très bien décider de changer d’activité ou même d’application. Il n’est donc pas sûr que le TextView soit toujours visible et disponible. Cette implémentation peut produire des problèmes de fuite de références (references leak).
Utilisation de la bibliothèque Volley¶
Volley est une bibliothèque Android pour le traitement de requête HTTP. En plus d’un support pour la création et l’analyse des réponses, elle fournit un support complet intégrant le chiffrement pour les connexions https, la gestion d’un cache client, la gestion concurrente dans l’émission de requêtes… Volley est également simple à mettre en place et à utiliser.
Ajout de la dépendance¶
Comme il s’agit d’une bibliothèque tierce, nous devons ajouter une dépendance
dans le fichier build.gradle
du module qui va utiliser Volley.
dependencies {
// ...
implementation 'com.android.volley:volley:1.1.1'
// ...
}
Émission d’une requête¶
Toutes les requêtes HTTP sont émises par Volley en tâche de fond de manière asynchrone. Ces requêtes sont mises dans une file d’attente et sont traitées au fur et à mesure par Volley. Dans notre code, nous devons commencer par créer une file d’attente de type RequestQueue pour pouvoir ensuite ajouter nos requêtes.
RequestQueue requestQueue = Volley.newRequestQueue(this);
Une requête est représentée par la classe Request<T>
ou une des classes qui
en héritent. Dans notre premier exemple, nous allons utiliser la classe
StringRequest
qui attend un résultat sous la forme d’une chaîne de caractères.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | String url = "https://tools.ietf.org/rfc/rfc7230.txt";
StringRequest request = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// traitement en cas de succès
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// traitement en cas d'échec
}
});
requestQueue.add(request);
|
De la ligne 2 à 13, nous créons la requête en passant quatre paramètres au
constructeur : la méthode HTTP à utiliser (dans cet exemple, GET), l’URL de
la requête, un objet listener pour le traitement du résultat de la requête en
cas de succès et un objet listener pour le traitement en cas d’échec.
Volley masque pour nous l’envoi de la requête, le traitement asynchrone et la
transformation de la réponse. En cas de succès, la méthode du listener reçoit
en paramètre de la méthode onResponse
le texte reçu. Pour envoyer cette
requête, il suffit de l’ajouter à la file d’attente (ligne 14).
Nous pouvons adapter ce code pour l’introduire dans une activité. Comme la file d’attente est réutilisable, nous pouvons la créer au lancement de l’activité et la conserver comme attribut.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | package dev.gayerie.monappli;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;
public class VolleyInternetActivity extends AppCompatActivity {
private static final Object REQUEST_TAG = new Object();
private RequestQueue requestQueue;
private TextView infoView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_volley_internet);
infoView = findViewById(R.id.infoView);
requestQueue = Volley.newRequestQueue(this);
}
@Override
protected void onResume() {
super.onResume();
String url = "https://tools.ietf.org/rfc/rfc7230.txt";
StringRequest request = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
infoView.setText(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
infoView.setText("Erreur de téléchargement : " + error.getMessage());
}
});
infoView.setText("Chargement en cours...");
request.setTag(REQUEST_TAG);
requestQueue.add(request);
}
@Override
protected void onPause() {
super.onPause();
requestQueue.cancelAll(REQUEST_TAG);
}
}
|
La file d’attente est créée ligne 26 au moment de la création de l’activité.
La requête HTTP est émise pour récupérer un texte qui doit être affiché dans un
TextView (lignes 32 à 47). À la ligne 46, nous ajoutons une étiquette à la
requête grâce à la méthode setTag
. Cela permet de créer un groupe de requêtes
qu’il est possible d’annuler. Ligne 53, lorsque l’activité est mise en pause,
nous appelons la méthode cancelAll
afin d’annuler une éventuelle requête en
attente qui n’aurait pas encore aboutie.
Émission d’une requête pour une réponse JSON¶
Une application Android est un client parfait pour une API Web. Les API Web sont destinées à fournir sur le Web des informations traitables par des programmes. La plupart du temps, ces informations sont échangées sous le format de représentation JSON car il est facilement traitable par programmation.
Volley fournit deux spécialisations de la classe Request<T>
: la classe
JsonObjectRequest
qui attend en objet JSON en réponse et la classe
JsonArrayRequest
qui attend un tableau JSON en réponse.
À titre d’exemple, nous allons utiliser l’API Géo mise en ligne par l’administration publique pour fournir des informations sur les communes, les départements et les régions en France.
Par exemple, si vous émettez la requête HTTP suivante :
Vous obtiendrez le réponse suivante :
{
"nom": "Gironde",
"code": "33",
"region": {
"code": "75",
"nom": "Nouvelle-Aquitaine"
}
}
Pour en savoir plus sur cette API, vous pouvez vous référer à la documentation officielle.
La classe JsonObjectRequest
offre un constructeur qui attend quatre
paramètres :
Une constante représentant la méthode HTTP à utiliser
L’URL de la ressource
Un objet de type JSONObject représentant les données à envoyer dans la requête (ou
null
si on ne souhaite pas envoyer de données)Un listener de type
Listener<JSONObject>
pour traiter une réponse en succèsUn listener de type
ErrorListener
pour traiter une réponse en échec
La classe JSONObject est fournie par l’API Android est permet de construire ou de consulter un document JSON en Java.
Nous pouvons déclarer une activité en utilisant le layout suivant :
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".RegionActivity">
<EditText
android:id="@+id/codeDepartement"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="number"
android:maxLength="2"
android:hint="Code département" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Rechercher"
android:onClick="search"/>
<TextView
android:id="@+id/departementResultat"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</androidx.appcompat.widget.LinearLayoutCompat>
Ce layout très simple déclare un champ de saisi texte pour le code du département,
un bouton pour lancer la recherche et un TextView pour afficher le résultat de
la recherche. Le bouton répond à l’événement click
en invoquant la méthode
search
de l’activité.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | package dev.gayerie.monappli;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import org.json.JSONException;
import org.json.JSONObject;
public class RegionActivity extends AppCompatActivity {
private static final Object REQUEST_TAG = new Object();
private static final String URL_PATTERN = "https://geo.api.gouv.fr/departements/%s?fields=nom,code,region";
private RequestQueue requestQueue;
private EditText codeDepartementEdit;
private TextView departementResultatView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_region);
codeDepartementEdit = findViewById(R.id.codeDepartement);
departementResultatView = findViewById(R.id.departementResultat);
requestQueue = Volley.newRequestQueue(this);
}
@Override
protected void onStop() {
super.onStop();
cancelRequest();
}
private void cancelRequest() {
if (requestQueue != null) {
requestQueue.cancelAll(REQUEST_TAG);
}
}
public void search(View view) {
String codeDepartement = this.codeDepartementEdit.getText().toString();
if (codeDepartement.isEmpty()) {
return;
}
if (codeDepartement.length() == 1) {
codeDepartement = "0" + codeDepartement;
}
sendRequest(codeDepartement);
}
private void sendRequest(String codeDepartement) {
cancelRequest();
departementResultatView.setText("Chargement...");
String url = String.format(URL_PATTERN, codeDepartement);
JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
fillDepartementResultat(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
if(error.networkResponse != null
&& error.networkResponse.statusCode == 404) {
departementResultatView.setText("Pas de département trouvé. Veuillez vérifier le code saisi.");
} else {
departementResultatView.setText("Erreur : " + error.getMessage());
}
}
});
request.setTag(REQUEST_TAG);
requestQueue.add(request);
}
private void fillDepartementResultat(JSONObject jsonObject) {
try {
String nomDepartement = jsonObject.getString("nom");
String codeDepartement = jsonObject.getString("code");
JSONObject jsonRegion = jsonObject.getJSONObject("region");
String nomRegion = jsonRegion.getString("nom");
String msg = String.format("%s est le département avec le code %s. Ce département fait partie de la région %s.", nomDepartement, codeDepartement, nomRegion);
departementResultatView.setText(msg);
} catch (JSONException e) {
departementResultatView.setText("Erreur : " + e.getMessage());
}
}
}
|
La méthode onCreate
(lignes 24 à 31) récupère une référence sur les principaux
composants du layout et crée une RequestQueue
pour Volley. La méthode
search
(lignes 45 à 54) qui est appelée lorsque l’utilisateur clique sur le
bouton, récupère l’information saisie et appelle la méthode privée sendRequest
.
La méthode sendRequest
(lignes 56 à 79) réalise la création de la requête
de type JsonObjectRequest
et l’ajoute dans la file d’attente de Volley.
Si la requête est un succès, le listener appelle la méthode fillDepartementResultat
(ligne 64). Enfin la méthode fillDepartementResultat
(lignes 81 à 92)
extrait les informations de l’objet JSON pour construire le message à afficher
dans la TextView.
Note
Pour envoyer des données au serveur dans la requête, il suffit de passer
un objet de type JSONObject comme troisième paramètre à la construction
d’une instance de JsonObjectRequest
. Imaginons que l’on dispose d’une API
Web permettant d’inscrire un nouvel utilisateur en envoyant son prénom et
son nom dans une requête POST. Le document JSON transmis serait de la forme :
{
"prenom": "David",
"nom": "Gayerie"
}
Nous pouvons déclarer une méthode pour gérer cette requête avec Volley :
private void inscrire(String prenom, String nom) throws JSONException {
String url = "https://inscription.formation.com/api";
JSONObject requestPayload = new JSONObject();
requestPayload.put("prenom", prenom);
requestPayload.put("nom", nom);
JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, requestPayload,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
// code en cas de succès
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// code en cas d'échec
}
});
this.requestQueue.add(request);
}