Accueil Nos publications Blog Android et les webservices : comment partir sur de bonnes bases ?

Android et les webservices : comment partir sur de bonnes bases ?

Il était une fois, un petit androïde curieux, adorant récupérer de goûteuses données sur internet pour les changer en de nouvelles données encore meilleures.

Cependant le petit androïde est triste : il n’arrive pas à récupérer ses données facilement.

Parfois, ça le ralentit.

Parfois, quand il demande la donnée, son appel n’a pas de réponse.

Parfois, même cela fait complètement crasher le petit androïde !

Du coup, ce pauvre petit androïde se demande comment il peut s’en sortir…

 

…Et c’est bien le but de cet article !

 

Bonjour, si vous avez cliqué sur ce lien c’est probablement que vous vous intéressez un tantinet à la programmation Android, ou bien que vous avez déjà commencé dans le domaine mais que vous souhaitez approfondir vos connaissances sur la communication réseau. Ou bien que vous êtes simplement curieux (et la curiosité, c’est bien !).

Bonne nouvelle, cet article est fait pour vous dans tous les cas. Le but ici est de brosser un tableau sur l’accès à un service web sur Android. Mais aussi surtout de savoir comment le faire bien.

On va donc reprendre pas à pas comment s’effectue un appel de webservice, en y ajoutant quelques subtilités pour aller “un peu plus loin” que la simple réussite de l’appel. Le seul pré-requis est d’avoir une bonne connaissance du Java et les bases d’Android (les activités et leur cycle de vie notamment).
Si vous souhaitez tester le code mis en exemple dans les différentes parties, vous pourrez le faire grâce à ce projet Android Studio.

Il s’agit d’une application de test, possédant déjà tout le nécessaire pour exécuter le programme, séparée en plusieurs modules. Ce projet de test sera évoqué à plusieurs reprises dans l’article.

 

Enfin, les personnes connaissant déjà les principes de base d’un appel de webservice en Java / Android peuvent passer directement au chapitre “Pour aller plus loin“.

 

Contexte

 

Commençons par une question simple, pour les grands débutants : qu’est-ce qu’un service web (ou webservice) ?

Il s’agit d’un programme, accessible depuis un serveur, permettant de faire communiquer un serveur avec une autre machine et, plus particulièrement, de mettre à disposition une ou plusieurs ressources du serveur. Pour simplifier à l’extrême, on peut voir le webservice comme une fonction accessible en ligne, permettant de modifier des ressources ou de les récupérer (plus de détails ici).

Il existe globalement deux catégories :

  • Les WebServices REST, qui respectent les standards du protocole HTTP avec une syntaxe / sémantique proche
  • Les WebServices dit “WS-”, qui suivent des standards tels que SOAP et WSDL, plus anciens et basés sur une communication par fichiers XML.

La majorité des webservices utilisés sur mobile sont aujourd’hui des services REST. Grâce au protocole HTTP, ils permettent de transmettre les données sous un plus grand nombre de formats (JSON, XML, HTML…) et sont également plus intuitifs, car l’appel se fait de façon similaire à une URL HTTP.

Cependants si je parle des services WS- (souvent surnommés services SOAP du nom du protocole qu’ils suivent majoritairement), c’est parce qu’un certain nombre de ces services existent encore au sein de certaines structures et que leur utilisation diffère des services REST.

 

Alors, pourquoi parle-t-on aussi souvent de webservices quand on touche à la mobilité ? C’est simple : il s’agit de la manière la plus performante pour récupérer les données issues d’un serveur.

Une différence fondamentale entre une application mobile et un site web est la quantité de données à envoyer depuis le serveur. Sur un site web, le serveur envoie l’affichage au complet (HTML, CSS), avec le comportement (Javascript etc.) et les données déjà intégrées dans le visuel. Sur une application mobile, le client possède déjà l’affichage et le comportement. Il ne lui manque que les données. Et c’est le rôle des webservices de lui fournir ces données pour que l’application puisse fonctionner.

 

Cela peut sembler évident ou inutile à certains, alors pourquoi je le rappelle ici ? Eh bien ça part d’un constat : la première erreur dans laquelle on tombe quand on débute le développement mobile, c’est de le comparer au développement web et au développement de client lourd. Dans les deux cas, la comparaison est fausse.

  • D’une part, parce que nous développons pour des appareils avec des ressources limitées: sur des devices Android, nous sommes loin d’égaler la puissance d’un PC en terme de mémoire, et l’accès au réseau est très aléatoire, parfois même impossible. Sans parler de la bande passante limitée.
  • D’autre part, parce que si les gens se tournent vers les applications mobiles, c’est principalement pour leur vitesse et leur fluidité comparées aux pages web sur mobile (dont la lenteur découle du point précédent). D’ailleurs, lorsque les deux existent (application et site web responsive), les utilisateurs préfèrent généralement passer par une application. Et si vous possédez vous-même un smartphone, vous comprendrez de quoi je parle.

 

Ceci posé, on comprend mieux à quel point une bonne gestion des webservices est cruciale dans une application : d’elle dépend une grande partie de l’expérience utilisateur, en termes de fluidité et de fiabilité. Une mauvaise gestion et l’application “plante”, les retours d’appels sont perdus, les données ne sont pas cohérentes… Et pour ajouter un peu de sel à tout ça, l’utilisateur mobile n’est pas des plus patients. Il n’hésitera pas à désinstaller une application qui lui déplaît. Et rapidement.

Aussi, dès le départ, prenons les bonnes pratiques pour éviter ce genre d’écueils, et voyons comment partir sur de bonnes bases !

 

Comment se déroule un appel ?

 

La base du processus

Comme je disais plus haut, en simplifiant à l’extrême, un webservice peut se résumer à appeler une fonction en ligne. On lui envoie ou non des paramètres, cela change l’état du modèle sur le serveur (comprendre : met à jour la base de données le plus souvent) et/ou renvoie des résultats.

Voilà pour la version bête et méchante. Dans les faits, c’est un poil plus complexe :

  • La “fonction” à appeler est en réalité une URL à contacter. Dans le cas des services REST, cela sera l’adresse du webservice spécifique, pour le cas du SOAP ce sera l’adresse de l’API de webservices fournie par le serveur, le nom du webservice étant fourni en paramètre
  • Les paramètres doivent ensuite être formatés d’une certaine manière à l’envoi : paramètres ajoutés à l’URL et formattés en JSON (pour REST notamment), ou encore envoyés intégralement en XML (pour SOAP en général).
  • Enfin le retour du webservice a un format particulier également, en général du JSON ou du XML.

 

Jusque-là on parle du concept en général. Maintenant voyons voir ce que ça donne côté code, avec ce qu’Android peut fournir de plus basique pour effectuer l’appel : HttpUrlConnection.

Il va s’agir ici d’aller contacter un webservice sur GitHub permettant de récupérer la liste des repositories d’un utilisateur. Les explications sur le fonctionnement d’UrlConnection sont directement dans les commentaires du code :



private static final String URL_GITHUB_ENDPOINT = "https://api.github.com/";
private static final String USERNAME_GITHUB = "octocat";
private static final int NO_STATUS_CODE = 0;

public void callWebService(Context context, IWebserviceCallback callback) {
    URL url;
    int statusCode = NO_STATUS_CODE;
    WebserviceResult webserviceResult = null;
    long startTime = System.currentTimeMillis();
    try {
        // On compose l'Url à appeler (ici, pour récupérer
        // la liste des repositories d'un utilisateur)
        String completeUrl = URL_GITHUB_ENDPOINT + "users" + USERNAME_GITHUB;
        url = new URL(completeUrl);

        // 1er log important pour savoir ce qui est appelé après la composition de l'URL
        Log.i(LOG_TAG, "URL contacté : " + completeUrl);

        // On crée l'instance de HttpUrlConnection
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // Démarrage du téléchargement
        connection.connect();

        // Une fois le téléchargement effectué, on récupère le code http
        // renvoyé par le serveur
        statusCode = connection.getResponseCode();

        // 2eme log important pour savoir comment s'est déroulé l'appel
        Log.i(LOG_TAG, "Téléchargement terminé, code http : " + statusCode);

        // On vérifie si le status code est 200, ce qui signifie que
        // le serveur a pu exécuter correctement le webservice.
        if(statusCode == 200) {

            // Puis on récupère la donnée téléchargé sous la forme d'un InputStream
            InputStream inputStream = connection.getInputStream();

            // ...que l'on transforme ici en String par simplicité d'usage (note :
            // il peut s'agit d'autre chose qu'un String pour
            // d'autres webservices, comme des images)
            String data = readStringData(inputStream);

            // On mesure le temps d'appel jusqu'ici
            long duration = System.currentTimeMillis() - startTime;

            // On génère un objet custom "WebserviceResult" pour contenir la donnée,
            // le status code et la durée de l'appel.
            webserviceResult = WebserviceResult.newSuccessInstance(data, statusCode, duration);

        } else {
            // Si le code http est différent de 200, on notifie de l'erreur en génèrant
            // un objet custom "WebserviceResult".
            webserviceResult = WebserviceResult.newFailureInstance(statusCode);
        }

    } catch (Exception exception) {
        // On génère un objet custom "WebserviceResult" pour contenir l'exception et
        // le status code.
         webserviceResult = WebserviceResult.newFailureInstance(exception, statusCode);
    } finally {

        // Enfin, on renvoi l'objet résultat créé à la classe appelante, par le biais
        // d'une interface custom IWebserviceCallback
        callback.onWebserviceEnded(webserviceResult);
    }
}

/**
 * Cette méthode transforme les données binaires d'un InputStream en donnée de type String
 *
 * @param stream le flux d'entrée
 * @return la donnée au format String
 */
private String readStringData(InputStream stream)  {
    BufferedReader reader = null;
    StringBuilder result = new StringBuilder();
    try {
        reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));

        String line;
        while((line = reader.readLine()) != null) {
            result.append(line);
        }
        return result.toString();

    } catch (IOException e) {
        Log.e(getClass().getSimpleName(), "", e);

    } finally {
        // On ferme tout les flux dans tout les cas
        if(reader != null){
            try {
                reader.close();

            } catch (IOException exp2) {
                Log.e(getClass().getSimpleName(), "", exp2);
            }
        }
    }
    return null;
}

Important !

Lorsque vous effectuez une requête réseau sur Android, vous avez besoin d’ajouter dans le fichier AndroidManifest.xml de l’application les permissions :


<uses-permission android:name="android.permission.INTERNET">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE">

La première autorise l’application à accéder à internet, l’autre l’autorise à obtenir des informations sur le réseau utilisé.

Cela vous permettra d’effectuer l’appel sans avoir l’erreur suivante :

java.lang.SecurityException: Permission denied (missing INTERNET permission?)
 

Si vous le souhaitez, vous pouvez à présent lancer l’application de test, puis appuyer sur le bouton “Launch Webservice” dans l’onglet “First Webservice” (qui exécute le code de la classe FirstWebserviceCall).

Et là, vous obtenez…

Un joli “NetworkOnMainThreadException”.

 

Pourquoi mon application crash-t-elle ?

Ici, j’en reviens au premier point que j’avais abordé dans la partie “Contexte” : sur mobile, les ressources sont limitées ! La disponibilité et la vitesse du réseau notamment. Il est rare qu’une application nécessitant du réseau fonctionne dans les campagnes peu couvertes ou bien dans le métro (exemple : quand vous regardez Facebook le matin en allant au travail).

 

A ceci se rajoute une particularité de Java, et par extension d’Android : l’UI Thread (aussi appelé Main Thread). Toutes les actions se déroulent séquentiellement dans ce thread. Le fait de changer d’écran, d’animer une icône de chargement… et l’appel réseau lui-même. De fait, vu que les actions sont les unes à la suite des autres, si on veut effectuer un changement visuel (une animation par exemple) après le lancement de l’appel, il faut que celui-ci se termine avant de passer à la suite. Et ça peut être long. Très long…

Du coup, depuis la version 3.0 d’Android (API 11), il est défendu d’exécuter un appel réseau depuis le Main Thread. D’où l’apparition de ce “NetworkOnMainThreadException.

 

La solution à ça : l’asynchronisme. On exécute le code dans un processus parallèle, pour laisser l’UI Thread gérer tranquillement les actions graphiques. Et pour ça, il existe pas mal de solutions sous Android.

La méthode la plus directe (pour quelqu’un qui vient du monde Java en tout cas), c’est d’utiliser la classe Thread. Et… c’est aussi sans doute la moins pratique. Plusieurs raisons à cela :

  1. La gestion des paramètres d’entrée manque de souplesse (passage des paramètres à la construction du Thread). 
  2. Le thread n’est pas relié au cycle de vie de l’Activity, ce qui peut causer des pertes de données ainsi que des fuites mémoires (lors de la destruction de l’Activity notamment).
  3. Enfin, sur Android, on ne peut pas modifier de composants graphiques depuis un autre thread que l’UI Thread ! (Remarque : c’est d’ailleurs une des grosses différences avec Java/Swing, beaucoup plus permissif sur la question). Et comme on aimerait que le retour du webservice soit affiché un jour ou l’autre, ça pose problème. Il est nécessaire alors d’utiliser un Runnable donnant accès à la donnée vers l’UI Thread. Ce qui alourdit un peu le code…

 

Bref, je vous propose une méthode alternative qui résout au moins deux des problèmes mentionnés : l’ASyncTask.

Cette classe gère le multithreading de façon un peu hybride. Elle contient trois méthodes principales : une pour préparer les paramètres (onPreExecute), une pour exécuter l’action (doInBackground) et une qui récupère de résultat de l’action (onPostExecute).

Sur ces trois méthodes, seul doInBackground s’exécute dans un autre thread que l’UI Thread. L’objet gère lui-même le transfert de la donnée vers la méthode onPostExecute, qui réalise son traitement dans l’UI Thread.

 

Un exemple parle mieux que des mots, aussi voici un bout de code pour expliquer son fonctionnement :


private static final String URL_GITHUB_ENDPOINT = "https://api.github.com/"; 
private static final String USERNAME_GITHUB = "octocat"; 
private static final int NO_STATUS_CODE = 0;

public void callWebService(Context context, IWebserviceCallback callback) {
    GitHubWebserviceASyncTask aSyncTask = new GitHubWebserviceASyncTask(callback);

    // On exécute l'ASycnTask en lui donnant en paramètre le username souhaité
    aSyncTask.execute(USERNAME_GITHUB);

    // (Note : cette séparation n'est pas essentielle ici, elle est principalement
    // faite por montrer un exemple d'utilisation des paramètres pour l'ASyncTask)
}

/**
 * L'ASyncTask utilisé pour effectuer l'appel réseau vers l'API Github
 */
public class GitHubWebserviceASyncTask extends AsyncTask<String, Void, WebserviceResult>{

    private final IWebserviceCallback callback;

    public GitHubWebserviceASyncTask(IWebserviceCallback callback) {
        this.callback = callback;
    }

    @Override
    protected WebserviceResult doInBackground(String... params) {
        // Note : Toutes les opérations effectué dans cette méthode se font
        // dans un thread autre que l'UI Thread

        URL url;
        int statusCode = NO_STATUS_CODE;
        WebserviceResult webserviceResult;
        long startTime = System.currentTimeMillis();
        try {
            // Ici, le username est envoyé en paramètre d'entrée de l'ASyncTask
            String username = params[0];
            if(username == null) {
                throw new InvalidParameterException(
                        "Missing parameter for ASyncTask execution");
            }
            String completeUrl = URL_GITHUB_ENDPOINT + "users/" + USERNAME_GITHUB;
            url = new URL(completeUrl);
            Log.i(LOG_TAG, "URL contacté : " + completeUrl);

            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            connection.connect();

            statusCode = connection.getResponseCode();
            Log.i(LOG_TAG, "Téléchargement terminé, code http : " + statusCode);

            if(statusCode == 200) {
                InputStream inputStream = connection.getInputStream();

                String data = readStringData(inputStream);

                long duration = System.currentTimeMillis() - startTime;

                webserviceResult = WebserviceResult.newSuccessInstance(
                        data, statusCode, duration);
            } else {
                webserviceResult = WebserviceResult.newFailureInstance(statusCode);
            }

        } catch (Exception exception) {
            webserviceResult = WebserviceResult.newFailureInstance(exception, statusCode);
        }

        return webserviceResult;
    }

    @Override
    protected void onPostExecute(WebserviceResult webserviceResult) {
        // Note : dans cette méthode, nous sommes retourné dans l'UI Thread

        // On renvoi le résultat de la requête à la classe appelante, par le biais de
        // l'interface custom IWebserviceCallback
        callback.onWebserviceEnded(webserviceResult);
    }
}

Ce code se retrouve dans la classe “AsynchronousWebserviceCall” du projet de test, code que vous pouvez exécuter dans l’onglet “Asynchronous Webservice”.

 

Pour aller plus loin

 

Aller plus loin avec HttpUrlConnection

Vous connaissez maintenant le déroulement standard d’un appel réseau avec HttpUrlConnection. Et si on ajoutait un niveau de complexité ?

La gestion des métadonnées. En effet, pour contacter le réseau, vous avez souvent besoin de transmettre certaines informations en plus pour accéder au serveur, gérer la configuration de l’appel ainsi que récupérer la réponse du serveur.

En sortie, il s’agira en général d’un code HTTP (200, 404 etc.) et d’un message d’erreur.

En entrée, ça peut être :

  • L’ajout d’un ou plusieurs entêtes, le plus commun étant le Content-Type pour les services REST.
  • Le port sur lequel s’établit la communication.
  • Une valeur de timeout, au-delà de laquelle on considère l’appel comme perdu.
  • Certaines informations propres à la technologie employée. Pour le REST par exemple, la méthode d’envoi HTTP (GET, POST, PUT…), pour du SOAP le lien sur le WSDL

Et potentiellement bien d’autres.

 

Pour aller encore un peu plus loin, il y a une chose que l’on n’a toujours pas effectuée ici : le post-traitement. Il se compose de deux éléments :

  • Le parsing: Votre appel réseau est terminé, mais les données que vous avez récupérées ne servent pas à grand-chose en l’état. Elles ont besoin d’être transformées en objets exploitables par l’application. C’est à ça que sert le parsing : transformer un texte en entité exploitable dans le programme. Ici, il s’agira de transformer un texte JSON en objet.
  • La persistance: Imaginons que l’utilisateur appelle plein de fois le même webservice. On aimerait éviter de devoir appeler le réseau à chaque fois (souvenez-vous : ressources limitées !). La solution est de persister les données reçues pour garder et exploiter les mêmes données tant qu’une mise à jour n’est pas demandée. Dans l’exemple, on utilisera une base de données de type ORM pour mapper les données reçues et réaliser ce stockage.

 


private static final String URL_GITHUB_ENDPOINT = "https://api.github.com/"; 
private static final String USERNAME_GITHUB = "octocat"; 
private static final int NO_STATUS_CODE = 0;
private static final int timeout = 10000; // 10s

@Override
public void callWebService(Context context, IWebserviceCallback callback) {
    // Avant de créer l'ASyncTask, on utilise le ConnectivityManager pour 
    // vérifier si le device est connecté à internet
    ConnectivityManager cm = (ConnectivityManager) context.getSystemService(
        Context.CONNECTIVITY_SERVICE);
    NetworkInfo activeNetwork = cm.getActiveNetworkInfo();

    // Si le réseau est inaccessible, on renvoi une erreur
    if(activeNetwork == null || !activeNetwork.isConnectedOrConnecting()){
        callback.onWebserviceEnded(WebserviceResult.newFailureInstance(NO_STATUS_CODE));
    } else {
        GitHubWebserviceASyncTask aSyncTask = new GitHubWebserviceASyncTask(callback);
        aSyncTask.execute(USERNAME_GITHUB);
    }
}

/**
 * L'ASyncTask utilisé pour effectuer l'appel réseau vers l'API Github
 */
public class GitHubWebserviceASyncTask extends AsyncTask<String, Void, WebserviceResult>{

    private final IWebserviceCallback callback;

    public GitHubWebserviceASyncTask(IWebserviceCallback callback) {
        this.callback = callback;
    }

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

            // Avant d'effectuer l'appel, on vérifie si l'objet à récupérer a déjà été téléchargé
            // et conservé en cache. Auquel cas, on n'effectue pas l'appel et on renvoi cet objet
            if(GitHubStorageManager.isUserInCache(username)){
                GitHubUser storedEntity = GitHubStorageManager.getStoredEntity(username);
                return WebserviceResult.newSuccessInstance(storedEntity, NO_STATUS_CODE, 0);
            }

            String completeUrl = URL_GITHUB_ENDPOINT + "users/" + USERNAME_GITHUB;
            url = new URL(completeUrl);
            Log.i(LOG_TAG, "URL contacté : " + completeUrl);

            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            // Cette méthode permet de préparer la connection en ajoutant certaines méta-données
            // comme le content-type, le timeout ou encore
            prepareConnection(connection);

            connection.connect();

            .........
    }

    @Override
    protected void onPostExecute(WebserviceResult webserviceResult) {
        // On exécute le post traitement de la requête (parsing en objet de la donnée XML et 
        // gestion du cache)
        GitHubUser gitHubUser = executePostTreatment(webserviceResult.getData().toString());

        // Si la donnée a été parsé avec succès, on re-crée un nouvel objet résultat contenant 
        // la donnée parsée.
        if(gitHubUser != null){
            webserviceResult = WebserviceResult.newSuccessInstance(
                gitHubUser, webserviceResult.getStatusCode(), webserviceResult.getDuration());
        }

        callback.onWebserviceEnded(webserviceResult);
    }
}

/**
 * Cette méthode permet de préparer la connection en ajoutant certaines méta-données
 *
 * @param connection l'instance d'HttpUrlConnection à utiliser pour le paramétrage
 */
private void prepareConnection(HttpURLConnection connection) throws IOException {
    // Indique qu'on consulte un service en GET (c'est la valeur par défaut, cette option est 
    // surtout utile pour faire un appel en POST ou en PUT)
    connection.setRequestMethod("GET");

    // Indique la durée maximum de l'appel avant de déclancher une erreur (timeout).
    // setConnectTimeout permet d'indiquer la durée maximale pour se connecter au serveur
    // setReadTimeout permet d'indiquer la durée maximale pour que le serveur renvoi ses données
    connection.setConnectTimeout(timeout);
    connection.setReadTimeout(timeout);

    // Indique le Content-Type utilisé
    connection.setRequestProperty("Content-Type", "application/json");
    // Note : il s'agit ici d'ajouter des meta-données au header de la requête, ce qui est aussi
    // souvent utilisé pour les systèmes d'authetification de type Basic Authentication.

    // Indique que la requête accepte l'envoi de paramètres (DoInput).
    connection.setDoInput(true);
    // Indique que la requête accepte l'envoi d'un "body" (DoOutput) (inutile ici)
    // connection.setDoOutput(true);
}

/**
 * Cette méthode effectue le post traitement sur la donnée serveur, à savoir le parsing de la donnée
 * et son stockage en cache.
 *
 * @param data la donnée reçu du serveur
 */
private GitHubUser executePostTreatment(String data){
    // On transforme la donnée serveur (JSON) en objet avec GSON
    GitHubUser gitHubUser;
    Gson gson = new Gson();
    try {
        gitHubUser = gson.fromJson(data, GitHubUser.class);

    } catch (JsonSyntaxException exception){
        // Si cette exception est levé cela signifie que Gson n'a pas réussi à parser la donnée serveur
        // (peut être dû
        Log.e(LOG_TAG, "failed to transcript json data to object", exception);
        return null;
    }

    // Une fois l'objet créé, on le persiste. Soit en base de données, soit dans un fichier.
    // Le système de cache étant un système complexe à mettre en place, ici on conserve
    // l'objet en mémoire vive pour simplifier
    GitHubStorageManager.storeEntity(gitHubUser);

    return gitHubUser;
}

 

L’appel commence à être assez exhaustif à présent !

 

Aller plus loin dans l’asynchronie

L’utilisation des ASyncTask pour appeler les webservices résout deux problèmes : la gestion des paramètres et le retour des données vers l’UI Thread. Il reste le troisième problème : la synchronisation avec le cycle de l’Activity. Et ce n’est pas un petit problème malheureusement.

Exemple : L’utilisateur est sur une Activity A. Vous lancez un appel réseau long, qui possède une référence sur cette Activity. L’appel est long, il décide d’aller voir ailleurs et lance l’Activity B. L’appel se termine, le résultat est renvoyé sur l’Activity A… qui a été détruite faute de mémoire suffisante après le passage sur B. Vous ne pouvez plus manipuler l’UI de cette Activity, et donc une exception est levée.

En plus ce ça, comme l’ASynctask possède une référence sur l’Activity A, celle-ci n’est pas ramassée par le garbage collector et reste en mémoire. Combo !

 

Du coup, il s’agit de voir s’il n’existe pas d’autres solutions pour gérer proprement cette problématique.

Voici une revue de quelques autres solutions :

  • ASyncTaskLoader : il s’agit d’un outil présent depuis Gingerbread (API 11)  permettant de synchroniser l’exécution de tâches longues avec une Activity ou un Fragment. Sa force réside dans son couplage fort avec le cycle de vie de l’Activity : l’appel s’effectue à la création de l’Activity (onCreate) et est annulé à sa destruction (onDestroy). Seul inconvénient : la tâche est relancée à chaque création d’activité
  • PriorityJobQueue : une bibliothèque pour Android qui permet d’ajouter une série d’appels asynchrones à la manière d’un ThreadPoolExecutor. Sa force réside dans sa diversité de configuration : gestion d’un cycle de vie, des priorités, persistance des données reçues… Et surtout, sa logique est découplée de celle des Activity. Une tâche peut être lancée par une Activity et son résultat récupérée par une autre.
  • RoboSpice : une autre bibliothèque offrant une solution élégante. Elle passe par un Service Android pour effectuer l’appel, prend en entrée une requête contenant les paramètres de l’appel ainsi qu’un listener utilisé pour obtenir le retour de l’appel, directement dans l’UI Thread. De plus, elle gère un système de cache permettant à une seconde Activity de récupérer le résultat, en cas de changement. La bibliothèque a été pensée de base pour les appels au réseau, proposant des modules intégrant certaines bibliothèques réseau connues. L’inconvénient ici est qu’il faut créer un nouveau module pour intégrer une bibliothèque réseau qui n’est pas gérée de base.

 

En parlant de bibliothèques réseau… saviez-vous qu’il en existe beaucoup ? Et de très pratiques ?

 

Pour aller plus loin Les API réseaux

En effet, HttpUrlConnection est sur Android la fonctionnalité de bas niveau, la base fournie pour communiquer en Http avec le réseau. Mais utiliser HttpUrlConnection, c’est comme monter des oeufs avec un fouet : c’est plus accessible, mais aussi plus long et compliqué.

Aussi, voyons voir ce qui peut nous servir de batteur électrique en fonction des besoins.

 

Note

Je fais un aparté sur Apache HttpClient, disponible directement dans l’API Android à l’instar de HttpUrlConnection. Il permet d’effectuer des opérations HTTP de base tout comme lui, et si j’en parle c’est que quelques tutoriaux sur internet s’en servent. Sauf que cette bibliothèque a été retirée par Google en API 23 (Android 6.0). La raison avancée est le manque de performance de la bibliothèque comparée à HttpUrlConnection. Cette bibliothèque étant amenée à disparaître, je vous déconseille de vous en servir.

 

Pour les webservices REST

Les webservices REST sont les principaux webservices avec lesquels on est amené à travailler sur Android, de par leur rapidité et leur simplicité d’usage, et du fait qu’ils soient les plus répandus aujourd’hui. De nombreuses bibliothèques existent, mais deux sortent du lot :

  • Retrofit, créée par Square et basée sur le client Http “OkHttp” de la même société
  • Volley, bibliothèque développée par Google pour Android.

Une chose importante au sujet de ces deux bibliothèques est que toutes deux intègrent leur propre gestion de l’asynchronisme et du cache des données, ce qui simplifie beaucoup la gestion des appels. Le seul problème avec ces API est la configuration poussée de certains paramètres, parfois impossible (ex: gestion de la durée du cache pour Volley).

 

Pour les webservices SOAP

Les webservices SOAP sont, comme dit plus haut, moins utilisés sur mobile. C’est pourquoi peu de bibliothèques existent pour gérer des webservices de ce type.

Néanmoins un nom apparaît souvent, c’est ksoap2. Il s’agit à l’origine d’une bibliothèque Java reprise pour Android, qui simplifie la création des nombreux paramètres de configuration à envoyer, notamment de “l’enveloppe” (un fichier XML décrivant le webservice à contacter, envoyé dans la requête).

 

Pour le téléchargement des images

Le téléchargement des images est une problématique particulière, car d’une part elle se fait souvent en synchronisation avec la création d’une vue, mais aussi parce que l’image a besoin d’être redimensionnée pour être affichée correctement.

Plusieurs bibliothèques, là encore, permettent ceci. Parmi les plus connues : Glide, UniversalImageLoader et Picasso.

Chacune de ces bibliothèques gère plusieurs aspects du téléchargement d’images : redimensionnement, gestion du cache, management d’une ImageView par rapport à cette image…

 

Avec les quelques bibliothèques décrites ici, vous devriez avoir quelques références que vous pourrez creuser le jour où vous chercherez à gérer des webservices dans votre application. 

 

Conclusion

 

Pour finir, mon objectif avec cet article était en quelque sorte de vous “ouvrir” aux webservices. Pas seulement de vous initier à un exemple simple, mais de vous montrer l’ensemble de la problématique et quelques solutions qui existent. Il s’agit d’un sujet vaste, complexe et en évolution constante. Et si la prochaine fois que vous aurez affaire à un webservice, vous vous dites “ok, ça peut créer pleins de problèmes alors je me pose cinq minutes et je cherche quelles solutions existent”, c’est que cet article aura bien fait son boulot !