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 RPCRemote Procedure Call ou, plus spécifiquement, de RMIRemote method invocation), 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.
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 URLUniform Resource Locator(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).
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.
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.
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().
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/.
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);
}
}
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.
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
(
);
}
}
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.
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"
);
}
}
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é.
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/