Appel de procédure distante sous Android

Olivier Le Goaer

Une application mobile moderne fonctionne rarement en vase clos ; elle nécessite au contraire de dialoguer avec des applications tierces. Nous sommes donc en présence d'une application répartie dans laquelle l'application mobile constitue une partie cliente qui peut ordonner des traitements de toutes sortes, localisés sur une partie serveur. Cette communication du client vers le serveur est recouvert par l'expression générique d'« appel de procédure distante », ou RPC. Ce tutoriel montre comment s'implémente ce mécanisme incontournable sous Android, à travers quelques exemples simples. Commentez

Les prérequis se trouvent dans le support de cours.

- Titre : Appel de procédure distante sous Android

- Auteur : Olivier Le Goaer

- Parution : 17 janvier 2015

- Mise à jour : 4 octobre 2016

- Public visé :

- Licence : Le contenu de cet article est rédigé par Olivier Le Goaer et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Appel de procédure distante

- II. Programmation client/serveur

- II-A. Appel asynchrone

- II-B. Le patron de conception

- III. Cas d'études

- III-A. Une image

- III-B. Du texte brut

- III-C. Rien du tout

- IV. La bibliothèque Volley

- IV-A. Retour sur l'exemple de l'image

- V. Code source

I - Appel de procédure distante

C'est un terme générique pour décrire la situation où un programme fait appel à des procédures d'un autre programme situé dans un autre contexte. C'est typiquement le cas entre un programme tournant sur un périphérique Android nomade et un programme tournant sur un serveur localisé à des milliers de kilomètres de là. Il n'existe pas à proprement parler de technologie native d'appel de procédure distante sous Android (pas de RPC ou, plus spécifiquement, de RMI), mais il est possible d'obtenir une solution équivalente sans trop d'efforts. À mi-chemin entre les technologies SOAP ou XML-RPC/JSON-RPC (haut niveau, mais complexe à mettre en place) et les sockets (beaucoup trop bas niveau), la solution la plus raisonnable est de se baser sur la couche transport réseau HTTP pour échanger des messages dont le format est totalement libre. Pour résumer, HTTP nous fournit un protocole fiable de type requête/réponse, le client et le serveur n'auront qu'à se mettre d'accord sur les données échangées. L'avantage reste que cette solution est totalement indépendante des plates-formes et des langages côté serveur : on parle parfois de « web service » pour y faire référence.

Architecture d'appel de procédure distante via HTTP

Le RPC Android a donc une multitude d'usages différents dans les applications, et sert notamment à la communication avec une base de donnée distante. En effet, il n'est pas prévu (ni souhaitable) qu'une application Android puisse se connecter directement à une base de donnée externe pour lui envoyer des ordres SQL. C'est là que le RPC entre en jeu pour contourner le problème car chaque action importante sur la BDD devra être pensée comme une procédure appelable coté serveur (/creerUnCompte, /supprimerProduit, /editerFacture, …).

Si les données d'une base de données distante sont consultables au format JSON (ce qui est très fréquent), vous pouvez utiliser genDROID, un générateur de code qui vous facilitera la vie et qui respecte le patron de conception proposé dans ce tutoriel.

II - Programmation client/serveur

S'adosser sur le protocole HTTP implique d'écrire des programmes dans des conteneurs web, c.-à-d. hébergés sur des serveurs HTTP (Apache HTTP, IIS, Node.js…), dans le langage de votre choix (PHP, ASP, ...). L'autre aspect étant que tout est considéré comme une ressource, et par conséquent chaque procédure appelable côté serveur est incarnée par une URL(1). Le passage des paramètres éventuels se fait classiquement par la méthode GET ou POST. Le retour du serveur a un format totalement libre même si les formats semi-structurés comme JSON ou XML conviennent à une majorité de situation (car facile à parser coté client).

Le présent tutoriel n'a pas pour objectif d'aborder les technologies web, une section dédiée à ces sujets étant par ailleurs disponible sur developpez.com. Retenez simplement que le but de tout programme web côté serveur est de produire une réponse HTTP quelconque à partir d'une requête HTTP du client, le plus souvent en exploitant le contenu d'une base de données.

Dorénavant, concentrons-nous sur ce qui doit être fait au niveau du client Android.

II-A - Appel asynchrone

Historiquement, la technologie d'appel distant RPC repose sur des appels synchrones : le client reste bloqué dans l'attente de la réponse de serveur. Mais cela s'avère fortement handicapant dans notre contexte où le réseau possède un temps de latence, quand il n'est pas carrément défaillant. En effet, sur Android il est primordial que les appels soient asynchrones puisque le rendu graphique des écrans (Activity) de l'application est réalisé par un processus dédié, le UI Thread, qui ne doit pas être mis en attente d'une hypothétique réponse d'un serveur. Si tel était le cas, cela signifierait un gel complet de l'interface utilisateur, ce qui n'est pas acceptable et déclenche typiquement une ANR (Application Not Responding).

Le popup ANR affiché à l'utilisateur

Ce problème est un tel fléau, que depuis Android 3.0, une exception spéciale est levée : la NetworkOnMainThreadException.

II-B - Le patron de conception

La plupart du temps nous avons un écran graphique d'une part, et un appel de procédure distante asynchrone d'autre part, le tout devant être resynchronisé à un moment ou un autre. Il se trouve que l'API Android introduit un élément qui va grandement nous faciliter la vie, pour peu qu'on l'utilise correctement : la tâche asynchrone (AsyncTask). Je vous propose donc le patron de conception ci-dessous, à adapter selon vos besoins.

Patron de conception pour un RPC Android (Notation UML)

Selon ce patron, votre classe MyScreen implémente une méthode populate() dont le rôle exclusif est de mettre à jour les vues avec les données obtenues en retour de l'appel distant. L'instanciation du RPC est réalisée ici dans onStart() de sorte que chaque fois que l'écran prend le focus, l'appel distant est déclenché : idéal pour être certain d'avoir des données fraîches. Bien sûr, vous pouvez déplacer cette instruction ailleurs.

MyScreen.java

public class MyScreen extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Mise en place des vues ici
}

@Override
protected void onStart() {
super.onStart() ;
//Ordonne l'appel asynchrone avec des arguments de type X
new MyRPC(this).execute(arg1, arg2, arg3, ...) ;
}

public void populate(Z data) {
//Mise à jour des vues avec data de type Z
}

}

Le code de l'appel distant sera quant à lui écrit dans votre classe générique MyRPC, paramétrée par trois types de votre choix :

- X : le type des données d'entrées pour réaliser l'appel ;

- Y : le type de l'unité de progression (le plus souvent un entier) ;

- Z : le type du résultat de retour de l'appel.

Une Asyntask possède les trois méthodes importantes onPreExecute(), doInBackground() et onPostExecute(), qui sont invoquées automatiquement dans cet ordre par le système Android dès lors que vous invoquez la méthode execute(). Il faut alors comprendre le cheminement suivant : ce que vous passez en entrée de execute() se retrouve en entrée de doInBackground() où une requête HTTP sera forgée via un client, et le retour de cette dernière est implicitement réinjecté en entrée de onPostExecute() pour lui appliquer un post-traitement. En l'occurrence, notre patron de conception stipule de faire remonter cette valeur jusqu'à l'écran via populate().

MyRPC.java

public class MyRPC extends AsyncTask<X, Y, Z> {

private volatile MyScreen screen;  //référence à l'écran
private HttpClient client;         //référence au client HTTP

public MyRPC(MyScreen s) {
this.screen = s ;
this.client = new DefaultHttpClient();
}

@Override
protected void onPreExecute() {
//prétraitement de l'appel
}

@Override
protected Z doInBackground(X… params) {
//Appel de procédure distante via HTTP (traitement long)
Z obj = this.client.execute(url_with_params);
return obj; //Objet de type Z automatiquement réinjecté en entrée de onPostExecute()
}

@Override
protected void onPostExecute(Z result) {
//post-traitement de l'appel
this.screen.populate(result); //callback
}

}

Lorsque la configuration du périphérique change (vous pivotez votre appareil, vous modifiez la langue de l'OS…), l'instance de l'« Activity » est automatiquement détruite et une nouvelle est recréée. Pour éviter que l'AsyncTask utilise une référence périmée (car mise en cache par la VM), le champ screen est déclaré volatile.

III - Cas d'études

Afin d'expérimenter le patron ci-dessus, considérons trois cas d'études différents. Chaque projet étant unique, les possibilités sont infinies, mais elles peuvent toutefois être catégorisées en trois grandes familles :

- la procédure appelée retourne un flux d'octets (image, sons, binaires en tout genre…) ;

- la procédure appelée retourne un document (texte, JSON, XML et ses dérivés…) ;

- la procédure appelée ne retourne rien.

Dans un souci de clarté, dans les portions de code qui vont suivre, le traitement des exceptions est réduit au minimum syndical. Un tutoriel entier est consacré à cet aspect. De même, les contrôles des codes de retour HTTP du serveur (200, 404, 403…) sont volontairement omis.

III-A - Une image

Imaginons un programme serveur qui renvoie une image lorsqu'il est appelé. Ce peut être une image stockée physiquement sur un serveur comme https://www.developpez.net/template/images/logo.png, ou — plus intéressant — une image générée à la volée en fonction de paramètres comme http://chart.apis.google.com/chart?cht=qr&chs=200x200&chl=Coucou.

Cette dernière URL doit être vue comme la signature de la fonction Byte chart(String cht, String chs, String chl) située sur la machine distante http://chart.apis.google.com/.

ImageScreen.java

public class ImageScreen extends Activity {

private ImageView viewer;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_activity);
this.viewer = (ImageView) findViewById(R.id.imageView1);
}

@Override
protected void onStart() {
super.onStart();
new DownloadImage(this).execute("qr","200x200","Coucou");
}

public void populate(Bitmap data) {
this.viewer.setImageBitmap(data);
}

}

DownloadImage.java

public class DownloadImage extends AsyncTask<String, Void, Bitmap> {

private static final String BASE_URL = "http://chart.apis.google.com/chart";

private volatile ImageScreen screen;
private HttpClient client;
private ProgressDialog progress;

public DownloadImage(ImageScreen s) {
this.screen = s;
this.client = new DefaultHttpClient();
this.progress = new ProgressDialog(this.screen);
}

@Override
protected void onPreExecute() {
this.progress.setTitle("Veuillez patienter");
this.progress.setMessage("Récupération de l'image en cours...");
this.progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
this.progress.show();
}

@Override
protected Bitmap doInBackground(String... params) {

Bitmap dummyImage = null;

try {

String url = String.format("%s?cht=%s&chs=%s&chl=%s", BASE_URL, params[0], params[1], params[2]);
HttpGet request = new HttpGet(url);
HttpResponse response = this.client.execute(request);
HttpEntity entity = response.getEntity();
byte[] bytes = EntityUtils.toByteArray(entity);
dummyImage = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

} catch (Exception e) { Log.e("RPC","Exception levée", e); }

return dummyImage;
}

@Override
protected void onPostExecute(Bitmap result) {

if(this.progress.isShowing()) this.progress.dismiss();
this.screen.populate(result);
}

}

III-B - Du texte brut

Imaginons maintenant un programme serveur qui renvoie du texte (poèmes, définitions, blagues…). En guise d'illustration, j'ai choisi l'URL http://loripsum.net/api/plaintext/short/20/ qui renvoie du texte aléatoirement. Bien entendu, les données auraient tout aussi bien pu être structurées en JSON ou en XML. Quel que soit le format, il vous faudra « parser » les données reçues. L'API Android contient tout ce qu'il faut pour cela, et notamment la classe Scanner quand il s'agit de texte libre comme c'est le cas ici.

LoremScreen.java

public class LoremScreen extends ListActivity {

private ArrayAdapter<String> adapter;

@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.lorem_activity);
this.adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
this.setListAdapter(adapter);
}

@Override
protected void onStart() {
super.onStart();
new DownloadLorem(this).execute();
}

public void populate(ArrayList<String> data) {
this.adapter.clear();
this.adapter.addAll(data);
this.adapter.notifyDataSetChanged();
}

}

DownloadLorem.java

public class DownloadLorem extends AsyncTask<Void, Void, ArrayList<String>> {

private static final String BASE_URL = "http://loripsum.net/api/plaintext/short/20/";
private LoremScreen screen;
private HttpClient client;
private ProgressDialog progress;

public DownloadLorem(LoremScreen s) {
this.screen = s;
this.client = new DefaultHttpClient();
this.progress = new ProgressDialog(this.screen);
}

@Override
protected void onPreExecute() {
progress.setTitle("Veuillez patienter");
progress.setMessage("Récupération des données en cours...");
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.show();
}

@Override
protected ArrayList<String> doInBackground(Void... params) {

InputStream content = null;
ArrayList<String> dummyTexts = new ArrayList<String>();

try {
HttpResponse response = this.client.execute(new HttpGet(BASE_URL));
content = response.getEntity().getContent();
Scanner scanner = new Scanner(new InputStreamReader(content));
scanner.useDelimiter("\\s*\n");
while (scanner.hasNext()) { dummyTexts.add(scanner.next().substring(0,30).concat("...")) ; }

} catch (Exception e) { Log.e("RPC","Exception levée", e); }

return dummyTexts;
}

@Override
protected void onPostExecute(ArrayList<String> result) {

if(progress.isShowing()) progress.dismiss();
this.screen.populate(result);
}

}

III-C - Rien du tout

Enfin, imaginons un programme serveur codé en PHP et qui stocke anonymement les démarrages et arrêts de votre application à des fins statistiques. Pour en avertir le serveur, l'URL de la procédure serait de la forme : http://my.company.com/remote/logUserConnexion?id=00000000-54b3-e7c7-0000-000046bffd97&event=CONNECT. Comme il n'y a pas de retour de l'appel distant, le callback populate() du patron de conception est inutile.

LogScreen.java

public class LogScreen extends Activity {

private TelephonyManager tm;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.log_activity);

tm = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);
new NotifyLogger().execute(tm.getDeviceId(), "CONNECT");
}

@Override
protected void onDestroy() {
super.onDestroy();
new NotifyLogger().execute(tm.getDeviceId(), "DISCONNECT");
}

}

NotifyLogger

public class NotifyLogger extends AsyncTask<String, Void, Void> {

private static final Object BASE_URL = "http://my.company.com/remote/logUserConnexion.php";

private HttpClient client;

public NotifyLogger() {
this.client = new DefaultHttpClient();
}

@Override
protected Void doInBackground(String... params) {

try {
String url = String.format("%s?id=%s&event=%s", BASE_URL, params[0], params[1]);
HttpGet request = new HttpGet(url);
this.client.execute(request);
} catch (Exception e) { Log.e("RPC","Exception levée", e); }

return null;
}

}

IV - La bibliothèque Volley

Le RPC Android par HTTP est devenu un aspect tellement crucial des applications mobile que Google a développé une libraire entièrement dédiée à ce mécanisme : Volley

Volley fournit une couche d'abstraction supplémentaire vis-à-vis du patron de conception présenté dans ce tutoriel :

- il supporte nativement le JSON, les images, et le texte brut ;

- il offre des performances plus poussés au niveau des threads (fiabilité, priorisation, annulation) et donc de l'utilisation du réseau ;

- il propose un « vrai » système de callbacks : en cas de succès et en cas d'échec de l'appel.

IV-A - Retour sur l'exemple de l'image

Afin d'illustrer les possibilités, reconsidérons le cas d'étude III.A où la procédure distante retourne une image. Puisque Volley gère nativement les images (mais il est possible créer ses propres types de réponse), le code est très compact à écrire et peut tenir dans le code de l'écran. Ci-dessous, seul le callback en cas de succès a été implémenté.

VolleyImageScreen.java

public class VolleyImageScreen extends Activity implements Response.Listener<Bitmap> {

private ImageView viewer;
private ProgressDialog progress;

@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.image_activity);
this.viewer = (ImageView) findViewById(R.id.imageView1);
}

@Override
protected void onStart() {
super.onStart();

this.progress = new ProgressDialog(this);
this.progress.setTitle("Veuillez patientez");
this.progress.setMessage("Récupération de l'image en cours...");
this.progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
this.progress.show();

// Instancie la file de message (cet objet doit être un singleton)
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://chart.apis.google.com/chart?cht=qr&chs=200x200&chl=Coucou";

// Requête d'une image à l'URL demandée
ImageRequest picRequest = new ImageRequest(url, this, 0, 0, null, null);
// Insère la requête dans la file
queue.add(picRequest);
}

@Override
public void onResponse(Bitmap response) { //callback en cas de succès

if(this.progress.isShowing()) this.progress.dismiss();
this.viewer.setImageBitmap(response);
}

}

V - Code source

La totalité du code source utilisé pour ce tutoriel est téléchargeable sous forme d'une archive de projet Eclipse. Elle contient déjà la bibliothèque Volley dans le répertoire /libs/

(1)C'est à ce stade que l'on peut décider d'appliquer les principes d'une architecture web REST ou non.