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.

Déclaration de la permission dans le fichier AndroidManifest.xml
<?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.

Exemple d’ouverture de connexion avec la classe URL
 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.

Exemple d’une AsyncTask pour le téléchargement de texte
 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é.

Appel de la tâche asynchrone depuis une 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.

Ajout de la dépendance à 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.

Création de la file d’attente
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.

Envoi d’une requête
 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.

Mise en place de Volley dans une 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
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 :

  1. Une constante représentant la méthode HTTP à utiliser

  2. L’URL de la ressource

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

  4. Un listener de type Listener<JSONObject> pour traiter une réponse en succès

  5. Un 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 :

Le layout de l’activité de consultation d’un département
<?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é.

L’activité de consultation d’un département
 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 :

Envoi de données au format JSON 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);
}