Publié par

Il y a 6 ans -

Temps de lecture 12 minutes

Android – Oubliez définitivement les AsyncTask avec RxJava

En tant que développeur Android, vous avez sûrement déjà été confronté aux limitations des AsyncTasks.

Peut-être avez-vous eu l’occasion de saisir dans votre moteur de recherche préféré les mots-clés "Orientation Change", "Memory Leak", "Error Handling" ou même "Running Parallel" suivis du célèbre "AsyncTask" ?

Si c’est le cas, lisez ce qui suit puisque nous allons vous montrer une nouvelle façon d’exécuter des traitements (par exemple des appels réseau) sur un thread séparé, tout en récupérant les résultats dans le thread principal, le tout sans parler de Thread, de Service, de Fragment, d’EventBus ou d’AsyncTask.

À la place, nous allons vous présenter RxJava.

Introduction à RxJava

RxJava est une implémentation réalisée par Netflix du projet Rx (Reactive Extensions) initialement conçu par Microsoft, et libéré en novembre 2012.
Ce projet se décrit comme une librairie permettant de composer des programmes asynchrones basés sur des événements en utilisant des séquences observables.

Rx est basé sur le paradigme de la programmation réactive fonctionnelle (Functional Reactive Programming)

Comme l’a très bien résumé markhudnall, la programmation réactive fonctionnelle n’est pas aussi compliquée que son nom le laisse entendre : ça parle essentiellement de flux et de données. Un flux produit des données à différents moments dans le temps. Un observateur est notifié lorsque des données sont récupérées du flux, et il peut ensuite les utiliser pour en faire quelque chose.

Pour plus d’informations sur ce sujet, nous ne saurons que trop peu vous conseiller de consulter les pages Wikipedia sur la "programmation fonctionnelle" [1], "  la "programmation réactive" [2], les "fonctions anonymes (et autres functional building blocks)" [3], et enfin la "programmation réactive fonctionnelle" [4].

En ce qui concerne Rx, le programmeur Dave Sexton le qualifie comme le remède à la programmation asynchrone spaghetti "If asynchronous spaghetti code were a disease, Rx is the cure.

En effet, peut-être avez-vous déjà été confronté à un problème appelé "Callback Hell" où un callback (par exemple le résultat d’une AsyncTask) appelle une autre méthode qui, dans son callback en appelle une autre qui, dans son callback modifie une vue. C’est moche ! Oui… Et RxJava peut aider sur ce point.

…Et concrètement… ?

Nous allons dans cet article utiliser RxJava pour réaliser une application Android communiquant avec l’API REST de GitHub

Le but de cette application sera de faire 3 requêtes sur l’API de GitHub pour récupérer les informations détaillées de 3 utilisateurs : "mojombo", "JakeWharton" et "mattt"

Pour simplifier le code de la partie REST, nous allons utiliser le client Retrofit.

Le service que nous allons utiliser est le suivant :

https://api.github.com/users/{username}

Il renvoie un résultat de ce type :

{
  "login": "mattt",
  "id": 7659,
  "type": "User",
  "followers": 3926,
}

Une fois les informations récupérées, nous afficherons dans une TextView pour chaque utilisateur, le nombre de ses followers. Le résultat aura la forme suivante :

[none ]mojombo: 16666 followers
JakeWharthon: 3638 followers
mattt: 3925 followers[/none]

Le layout principal de notre application n’est composé que d’un seul élément : une TextView. Voici son contenu :

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@+id/textview"
          android:layout_width="match_parent"
          android:layout_height="match_parent"/>

Implémentation de la partie REST

Notre projet a besoin de deux dépendances Gradle :

dependencies {
    compile 'com.squareup.retrofit:retrofit:1.3.0'
    compile 'com.netflix.rxjava:rxjava-android:0.15.1'
}
  • retrofit : le client REST que nous allons utiliser
  • rxjava-android : une addition à RxJava offrant des bindings spécifiques pour Android (nous verrons cela un peu plus tard). rxjava-android a lui-même pour dépendance rxjava-core, il est inutile de spécifier cette dépendance sous-jacente.

Nous pouvons également ajouter tout de suite la permission INTERNET dans notre AndroidManifest.xml

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

Notre modèle de données sera le suivant :

package fr.xebia.rxtuto.api;
import java.util.Locale;

public class GitHubMember {
    public String login;
    public int followers;

    @Override
    public String toString() {
        return String.format(Locale.US, "%s: %d followers", login, followers);
    }
}

Enfin, nous allons créer une classe ApiManager qui sera un helper aux appels REST.

package fr.xebia.rxtuto.api;

import retrofit.RestAdapter;
import retrofit.http.GET;
import retrofit.http.Path;

public class ApiManager {

    private interface ApiManagerService {
        @GET("/users/{username}")
        GitHubMember getMember(@Path("username") String username);
    }

    private static final RestAdapter restAdapter = new RestAdapter.Builder()
            .setServer("https://api.github.com")
            .build();

    private static final ApiManagerService apiManager = restAdapter.create(ApiManagerService.class);
}

Grâce à Retrofit (qui s’occupera d’effectuer la requête HTTP et le mappage JSON), l’implémentation de notre service REST est déjà quasiment terminée, il nous suffira d’appeler la méthode.

apiManager.getMember(String username);

dans un thread pour récupérer les informations détaillées d’un utilisateur. Dans notre cas, il faudra appeler cette méthode 3 fois de suite (1 appel pour chaque utilisateur)

Implémentation de la partie RxJava

Toujours dans la classe ApiManager, nous allons créer une méthode permettant de récupérer un objet GitHubMember à partir d’une chaîne de caractères représentant l’identifiant de l’utilisateur.
Une signature possible de méthode serait la suivante :

public static GitHubMember getGitHubMember(final String username);

Cependant, avec RxJava, on ne renvoie pas directement les objets concernés mais plutôt des Observables, qui seront manipulés par Rx.

Notre méthode sera donc écrite comme cela :

public static Observable<GitHubMember> getGitHubMember(final String username) {
    return Observable.create(new Observable.OnSubscribeFunc<GitHubMember>() {
        @Override
        public Subscription onSubscribe(Observer<? super GitHubMember> observer) {
            try {
                GitHubMember member = apiManager.getMember(username);
                observer.onNext(member);
                observer.onCompleted();
            } catch (Exception e) {
                observer.onError(e);
            }
            return Subscriptions.empty();
        }
    }).subscribeOn(Schedulers.threadPoolForIO());
}

Plutôt que de retourner un GitHubMember, on retourne un Observable<GitHubMember>

C’est dans le corps de la méthode onSubscribe que l’on effectuera les traitements désirés (ici, l’appel au service web REST). Cette méthode sera appelée lorsqu’un Observer s’inscrira à cet Observable

Nous pouvons préciser à l’aide de la méthode subscribeOn dans quel thread cette partie sera exécutée (ici dans le "ThreadPool for I/O").
Rx nous fournit deux Schedulers par défaut : un pour les opérations d’entrées/sorties (I/O) et un autre pour les calculs divers (threadPoolForComputation())
Cela permet d’éviter certains blocages éventuels (on est ainsi sûr, par exemple, qu’un calcul ne sera jamais bloqué par de l’I/O, même s’il y a déjà plusieurs threads I/O en attente).

Une fois l’appel effectué, on envoie son résultat dans observer.onNext() et on appelle observer.onCompleted() pour préciser que nous n’avons plus rien d’autre à faire après cette étape.
Pour finir, on retourne une souscription vide avec Subscriptions.empty(). On peut imaginer certains cas où il faut libérer certaines ressources quand un Observer se désinscrit de cet Observable. Dans ce cas, plutôt que de retourner une souscription vide, on pourra retourner une nouvelle souscription en spécifiant les ressources à désallouer dans la méthode dédiée :

return new Subscription() {
    @Override
    public void unsubscribe() {
        // code à exécuter lorsqu'un Observer se désinscrit
    }
}

Maintenant que notre Observable (celui qui exécute l’appel au service web) est créé, nous allons nous occuper de notre Observer (celui qui appelle les différents Observables et effectue certaines opérations une fois notifié de leurs résultats).

Ici, notre Observer sera notre Activity principale. On prendra soin d’implémenter l’interface Observer<T> et d’écrire les méthodes associées, qui sont :

  • onCompleted : lorsque tous les observables ont été appelés
  • onError : nous permet de gérer correctement les erreurs
  • onNext : appelé à chaque fois qu’un observable a fini son traitement. Cela nous permet de récupérer le résultat du traitement de l’observable, avant de passer au suivant.
public class MainActivity extends Activity implements Observer<GitHubMember> {

    private TextView mMembersView;
 
    /* [...] */
 
    @Override
    public void onCompleted() {
    }

    @Override
    public void onError(Throwable throwable) {
        Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onNext(GitHubMember member) {
        mMembersView.setText(String.format(Locale.US, "%s\n%s", member.toString(), mMembersView.getText()));
    }
}

Maintenant que notre Activity est prête à observer les objets Observable, il ne nous reste plus qu’à s’inscrire à ces différents observables.
Pour rappel, nous avons déjà écrit le code renvoyant un Observable permettant de récupérer les informations d’un utilisateur GitHub.
Puisque l’on souhaite récupérer les informations de 3 utilisateurs, et donc faire 3 requêtes, nous allons nous inscrire pour observer 3 Observables :

 

public class MainActivity extends Activity implements Observer<GitHubMember> {

    private TextView mMembersView;
    private Subscription mSubscription;

    private static final String[] GITHUB_MEMBERS = new String[]{"mojombo", "JakeWharton", "mattt"};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mMembersView = (TextView) findViewById(R.id.textview);

        mSubscription = Observable.from(GITHUB_MEMBERS)
                .mapMany(new Func1<String, Observable<GitHubMember>>() {
                    @Override
                    public Observable<GitHubMember> call(String s) {
                        return ApiManager.getGitHubMember(s);
                    }
                })
                .subscribeOn(Schedulers.threadPoolForIO())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this);
    }

    @Override
    protected void onDestroy() {
        mSubscription.unsubscribe();
        super.onDestroy();
    }

    /* [...] */
}
  • L’inscription de notre Observer aux 3 Observables est effectuée dans le onCreate.
  • On récupère tout d’abord un Observable<String> à partir des données de GITHUB_MEMBERS avec Observable.from()
  • La méthode mapMany() s’occupera ici de nous retourner un Observable<GitHubMember>, contenant nos 3 objets GitHubMember
  • C’est dans notre implémentation de Func1.call() que l’appel au service web sera effectué (c’est à cet endroit que retrofit exécutera la requête réseau et fera le mappage des données)
  • La méthode subscribeOn() indiquera que les Observables devront effectuer leurs traitements sur le threadpool I/O (un des avantages de RxJava est que l’on peut spécifier l’endroit où nos traitements seront exécutés)
  • La méthode observeOn() permet, grâce aux bindings spécifiques RxJava-Android, d’indiquer que l’on souhaite récupérer le résultat des Observables sur le thread Android principal avec AndroidSchedulers.mainThread()
  • Enfin, on s’inscrit avec la méthode subscribe(), et on n’oublie pas de se désinscrire dans le onDestroy() de l’activité.

Et voilà, RxJava va pouvoir effectuer 3 requêtes, et pour chaque requête va appeler MainActivity.onNext()

Un peu d’optimisation

À ce moment, vous avez pu remarquer plusieurs similitudes entre Rx et certains patrons de conception (Observateur, Iterateur). Nous n’avons pas encore vraiment parlé de l’aspect "programmation fonctionnelle" de RxJava. Si vous appréciez les fonctions d’ordre supérieur, vous apprécierez sûrement les différentes transformations (map, filter, reduce…) que l’on peut appliquer sur les Observables.

Dans l’exemple précédent, la méthode MainActivity.onNext() est appelée 3 fois, pour chaque réponse d’un Observable (puisque l’on a fait 3 requêtes).

Nous allons faire en sorte de modifier notre souscription afin qu’elle fasse toujours les 3 requêtes nécessaires, mais qu’entre temps, elle traite les données et qu’il n’y ait au final qu’un seul Observable et qu’un seul callback (onNext) qui soit appelé.

Voici notre nouvelle souscription :

mSubscription = Observable.from(GITHUB_MEMBERS)
        .mapMany(new Func1<String, Observable<GitHubMember>>() {
            @Override
            public Observable<GitHubMember> call(String s) {
                return ApiManager.getGitHubMember(s);
            }
        })
        .map(new Func1<GitHubMember, String>() {
            @Override
            public String call(GitHubMember gitHubMember) {
                return gitHubMember.toString();
            }
        })
        .aggregate(new Func2<String, String, String>() {
            @Override
            public String call(String s, String s2) {
                return s + "\n" + s2;
            }
        })
        .subscribeOn(Schedulers.threadPoolForIO())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(this);

Note : si vous utilisez IntelliJ, ce dernier simplifie l’affichage de l’implémentation, ce qui rend le code bien plus lisible :

mSubscription = Observable.from(GITHUB_MEMBERS)
    .mapMany((Func1) (s) -> {  return ApiManager.getGitHubMember(s); })
    .map((Func1) (gitHubMember) -> { return gitHubMember.toString(); })
    .aggregate((s, s2) -> { return s + "\n" + s2; })
    .subscribeOn(Schedulers.threadPoolForIO())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(this);
  • Grâce au from(), on récupère toujours un Observable<String> de 3 éléments
  • Le mapMany() fait les appels réseau et retourne un Observable<GitHubMember> de 3 éléments
  • Le map() retourne un Observable<String> contenant les 3 chaînes de caractères à afficher
  • Le aggregate() retourne un Observable<String> ne contenant plus qu’une chaîne de caractère combinant les 3 précédentes
  • Il n’y aura qu’un appel à onNext() avec directement la chaîne de caractères à affecter à la TextView
    @Override
    public void onNext(String member) {
        mMembersView.setText(member);
    }

Ici, grâce à RxJava et en quelques lignes, nous avons fait 3 requêtes réseau sur le thread I/O, nous les avons modifiées et combinées pour qu’à la fin, le callback ne soit appelé qu’une seule fois, sur le thread principal.
Nous n’avons manipulé directement ni AsyncTask, ni Service, ni Thread, ni Loader, ni Broadcast, ni EventBus. Le code (même si un peu verbeux à cause de la syntaxe Java 6) reste relativement court et centralisé.

Conclusion

Nous vous avons montré, dans cet article, qu’une infime partie de ce que peut faire RxJava, et nous espérons que cela vous donnera envie de vous pencher un peu plus sur ce sujet.

Le code source final de l’application est disponible sur ce lien

Pour plus d’informations, nous vous conseillons les liens suivants :

Publié par

Commentaire

10 réponses pour " Android – Oubliez définitivement les AsyncTask avec RxJava "

  1. Publié par , Il y a 6 ans

    Franchement, quel bordel pour faire trois fois rien ….

  2. Publié par , Il y a 6 ans

    Bonjour,
    Nous présentons ici une solution pour éviter les Callback Hells et avoir du code plus simple à maintenir et moins « spaghetti ».
    Cet outil peut s’avérer très utile pour des projets complexes où l’on ne contrôle pas entièrement l’API REST utilisée et où nous sommes obligés d’enchaîner ou d’imbriquer plusieurs appels réseau entre eux et de manipuler les résultats sur différents threads.
    Tout dépend du contexte et du point de vue.
    Ce n’est pas un hasard si Netflix et Soundcloud utilisent RxJava pour leurs projets mobiles.
    Si, effectivement, le but est de faire « trois fois rien », cette solution s’avère d’entrée trop complexe et des alternatives sont à privilégier.

  3. Publié par , Il y a 6 ans

    Très intéressant, merci pour cet article

  4. Publié par , Il y a 5 ans

    Bonjour,

    tout d’abord merci beaucoup pour l’article. Ensuite une petite question, est-ce qu’il est possible de se désinscrire d’une subscription? Et si oui comment?

  5. Publié par , Il y a 3 ans

    bonjour
    ce tuto est super interessant
    jessaies de l’adapter, mais j’ai une erreur sur le « GitHubMember » de return apiManager.GitHubMember(s); de MainActivity :
    GitHubMember >> cannot resolve method..
    qqun a t il eu l’erreur ?
    merci
    David

  6. Publié par , Il y a 3 ans

    Bonjour David,
    Attention, la méthode à appeler est « getGitHubMember(s) », et non pas « GitHubMember(s) » :)

  7. Publié par , Il y a 3 ans

    RxJava is mostly preferred for mobile app development and multitasking makes the process more fruitful. Along with multitasking, I preferring RxJava with Retrofit to boost mobile app performance.

  8. Publié par , Il y a 2 ans

    C’est vraiment génial mais c’est encore un peu compliqué pour moi. Pour le moment je reste avec les Asynctasks mais bientôt j’y passerai. Je pense que le soucis de la syntaxe peut en rebuter plus d’un.

  9. Publié par , Il y a 11 mois

    Wow, in simple words i can just say Complex thing explained in easiest possible way

  10. Publié par , Il y a 2 mois

    Thanks for sharing. Although I don’t know french and used Google Translate, I still managed to learn a lot. I will share it with my coworkers from android app development company I work for, Zaven.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.