Rencontrez RxJava : la bibliothèque de programmation réactive manquante pour Android
Publié: 2022-03-11Si vous êtes un développeur Android, il y a de fortes chances que vous ayez entendu parler de RxJava. C'est l'une des bibliothèques les plus discutées pour activer la programmation réactive dans le développement Android. Il est présenté comme le cadre incontournable pour simplifier les tâches simultanées/asynchrones inhérentes à la programmation mobile.
Mais… qu'est-ce que RxJava et comment « simplifie-t-il » les choses ?
Bien qu'il existe de nombreuses ressources déjà disponibles en ligne expliquant ce qu'est RxJava, dans cet article, mon objectif est de vous donner une introduction de base à RxJava et plus précisément comment il s'intègre dans le développement Android. Je donnerai également quelques exemples concrets et des suggestions sur la façon dont vous pouvez l'intégrer dans un projet nouveau ou existant.
Pourquoi envisager RxJava ?
À la base, RxJava simplifie le développement car il élève le niveau d'abstraction autour du threading. C'est-à-dire qu'en tant que développeur, vous n'avez pas trop à vous soucier des détails sur la façon d'effectuer les opérations qui doivent se produire sur différents threads. Ceci est particulièrement attrayant car le threading est difficile à maîtriser et, s'il n'est pas correctement implémenté, peut entraîner certains des bogues les plus difficiles à déboguer et à corriger.
Certes, cela ne signifie pas que RxJava est à l'épreuve des balles en matière de threading et il est toujours important de comprendre ce qui se passe dans les coulisses ; Cependant, RxJava peut certainement vous faciliter la vie.
Prenons un exemple.
Appel réseau - RxJava vs AsyncTask
Supposons que nous souhaitions obtenir des données sur le réseau et mettre à jour l'interface utilisateur en conséquence. Une façon de procéder consiste à (1) créer une sous-classe interne AsyncTask
dans notre Activity
/ Fragment
, (2) effectuer l'opération réseau en arrière-plan et (3) prendre le résultat de cette opération et mettre à jour l'interface utilisateur dans le thread principal .
public class NetworkRequestTask extends AsyncTask<Void, Void, User> { private final int userId; public NetworkRequestTask(int userId) { this.userId = userId; } @Override protected User doInBackground(Void... params) { return networkService.getUser(userId); } @Override protected void onPostExecute(User user) { nameTextView.setText(user.getName()); // ...set other views } } private void onButtonClicked(Button button) { new NetworkRequestTask(123).execute() }
Aussi inoffensive que cela puisse paraître, cette approche présente certains problèmes et limites. À savoir, les fuites de mémoire/contexte sont facilement créées puisque NetworkRequestTask
est une classe interne et contient donc une référence implicite à la classe externe. De plus, que se passe-t-il si nous voulons enchaîner une autre longue opération après l'appel réseau ? Nous aurions à imbriquer deux AsyncTask
ce qui peut réduire considérablement la lisibilité.
En revanche, une approche RxJava pour effectuer un appel réseau pourrait ressembler à ceci :
private Subscription subscription; private void onButtonClicked(Button button) { subscription = networkService.getObservableUser(123) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<User>() { @Override public void call(User user) { nameTextView.setText(user.getName()); // ... set other views } }); } @Override protected void onDestroy() { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } super.onDestroy(); }
En utilisant cette approche, nous résolvons le problème (des fuites de mémoire potentielles causées par un thread en cours d'exécution contenant une référence au contexte externe) en conservant une référence à l'objet Subscription
renvoyé. Cet objet Subscription
est ensuite lié à la méthode #onDestroy()
de l'objet Activity
/ Fragment
pour garantir que l'opération Action1#call
ne s'exécute pas lorsque l' Activity
/ Fragment
doit être détruit.
Notez également que le type de retour de #getObservableUser(...)
(c'est-à-dire un Observable<User>
) est chaîné avec d'autres appels. Grâce à cette API fluide, nous sommes en mesure de résoudre le deuxième problème lié à l'utilisation d'une AsyncTask
, à savoir qu'elle permet un chaînage supplémentaire des appels réseau/opérations longues. Plutôt chouette, hein ?
Plongeons plus profondément dans certains concepts RxJava.
Observable, Observer et Operator - Les 3 O de RxJava Core
Dans le monde RxJava, tout peut être modélisé sous forme de flux. Un flux émet des éléments au fil du temps, et chaque émission peut être consommée/observée.
Si vous y réfléchissez bien, un flux n'est pas un nouveau concept : les événements de clic peuvent être un flux, les mises à jour de localisation peuvent être un flux, les notifications push peuvent être un flux, etc.
L'abstraction de flux est implémentée à travers 3 constructions de base que j'aime appeler "les 3 O" ; à savoir : l' O bservable, l' O bserver et l'Operator. L' Observable émet des items (le flux) ; et l' Observateur consomme ces objets. Les émissions des objets observables peuvent en outre être modifiées, transformées et manipulées en enchaînant les appels de l'opérateur .
Observable
Un Observable est l'abstraction de flux dans RxJava. Il est similaire à un itérateur en ce sens que, étant donné une séquence, il parcourt et produit ces éléments de manière ordonnée. Un consommateur peut ensuite consommer ces éléments via la même interface, quelle que soit la séquence sous-jacente.
Disons que nous voulions émettre les nombres 1, 2, 3, dans cet ordre. Pour ce faire, nous pouvons utiliser la méthode Observable<T>#create(OnSubscribe<T>)
.
Observable<Integer> observable = Observable.create(new Observable.OnSubscribe<Integer>() { @Override public void call(Subscriber<? super Integer> subscriber) { subscriber.onNext(1); subscriber.onNext(2); subscriber.onNext(3); subscriber.onCompleted(); } });
L'appel de subscriber.onNext(Integer)
émet un élément dans le flux et, lorsque le flux a fini d'émettre, subscriber.onCompleted()
est alors appelé.
Cette approche de création d'un Observable est assez verbeuse. Pour cette raison, il existe des méthodes pratiques pour créer des instances Observable qui devraient être préférées dans presque tous les cas.
Le moyen le plus simple de créer un Observable consiste à utiliser Observable#just(...)
. Comme le nom de la méthode l'indique, elle émet simplement le ou les éléments que vous lui transmettez en tant qu'arguments de méthode.
Observable.just(1, 2, 3); // 1, 2, 3 will be emitted, respectively
Observateur
Le composant suivant du flux Observable est l'observateur (ou les observateurs) qui y sont abonnés. Les observateurs sont avertis chaque fois que quelque chose «d'intéressant» se produit dans le flux. Les observateurs sont avertis via les événements suivants :
-
Observer#onNext(T)
- invoqué lorsqu'un élément est émis depuis le flux -
Observable#onError(Throwable)
- invoqué lorsqu'une erreur s'est produite dans le flux -
Observable#onCompleted()
- invoqué lorsque le flux a fini d'émettre des éléments.
Pour vous abonner à un flux, appelez simplement Observable<T>#subscribe(...)
et transmettez une instance Observer.
Observable<Integer> observable = Observable.just(1, 2, 3); observable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { Log.d("Test", "In onCompleted()"); } @Override public void onError(Throwable e) { Log.d("Test", "In onError()"); } @Override public void onNext(Integer integer) { Log.d("Test", "In onNext():" + integer); } });
Le code ci-dessus émettra ce qui suit dans Logcat :
In onNext(): 1 In onNext(): 2 In onNext(): 3 In onNext(): 4 In onCompleted()
Il peut également y avoir des cas où nous ne sommes plus intéressés par les émissions d'un Observable. Ceci est particulièrement pertinent sous Android lorsque, par exemple, une Activity
/ un Fragment
doit être récupéré en mémoire.
Pour arrêter d'observer les éléments, nous devons simplement appeler Subscription#unsubscribe()
sur l'objet Subscription renvoyé.
Subscription subscription = someInfiniteObservable.subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } }); // Call unsubscribe when appropriate subscription.unsubscribe();
Comme on le voit dans l'extrait de code ci-dessus, lors de l'abonnement à un Observable, nous conservons la référence à l'objet d'abonnement renvoyé et appelons ultérieurement subscription#unsubscribe()
si nécessaire. Dans Android, il est préférable de l'invoquer dans Activity#onDestroy()
ou Fragment#onDestroy()
.
Opérateur
Les éléments émis par un observable peuvent être transformés, modifiés et filtrés via des opérateurs avant de notifier le ou les objets observateur souscrits. Certaines des opérations les plus courantes trouvées dans la programmation fonctionnelle (telles que mapper, filtrer, réduire, etc.) peuvent également être appliquées à un flux Observable. Prenons l'exemple de la carte :
Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() { @Override public Integer call(Integer integer) { return integer * 3; } }).subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } });
L'extrait de code ci-dessus prendrait chaque émission de l'Observable et la multiplierait par 3, produisant le flux 3, 6, 9, 12, 15, respectivement. L'application d'un opérateur renvoie généralement un autre Observable, ce qui est pratique car cela nous permet d'enchaîner plusieurs opérations pour obtenir le résultat souhaité.
Compte tenu du flux ci-dessus, supposons que nous voulions uniquement recevoir des nombres pairs. Ceci peut être réalisé en enchaînant une opération de filtrage .
Observable.just(1, 2, 3, 4, 5).map(new Func1<Integer, Integer>() { @Override public Integer call(Integer integer) { return integer * 3; } }).filter(new Func1<Integer, Boolean>() { @Override public Boolean call(Integer integer) { return integer % 2 == 0; } }).subscribe(new Observer<Integer>() { @Override public void onCompleted() { // ... } @Override public void onError(Throwable e) { // ... } @Override public void onNext(Integer integer) { // ... } });
Il existe de nombreux opérateurs intégrés à l'ensemble d'outils RxJava qui modifient le flux Observable ; si vous pouvez penser à un moyen de modifier le flux, il y a de fortes chances qu'il y ait un opérateur pour cela. Contrairement à la plupart des documentations techniques, la lecture de la documentation RxJava/ReactiveX est assez simple et précise. Chaque opérateur de la documentation est accompagné d'une visualisation de la manière dont l'opérateur affecte le flux. Ces visualisations sont appelées "diagrammes de marbre".
Voici comment un opérateur hypothétique appelé flip pourrait être modélisé à l'aide d'un diagramme en marbre :
Multithreading avec RxJava
Le contrôle du thread dans lequel les opérations se produisent dans la chaîne Observable se fait en spécifiant le planificateur dans lequel un opérateur doit se produire. Essentiellement, vous pouvez considérer un planificateur comme un pool de threads que, lorsqu'il est spécifié, un opérateur utilisera et exécutera. Par défaut, si aucun planificateur de ce type n'est fourni, la chaîne Observable fonctionnera sur le même thread où Observable#subscribe(...)
est appelé. Sinon, un planificateur peut être spécifié via Observable#subscribeOn(Scheduler)
et/ou Observable#observeOn(Scheduler)
dans lequel l'opération planifiée se produira sur un thread choisi par le planificateur.
La principale différence entre les deux méthodes est que Observable#subscribeOn(Scheduler)
indique à la source Observable sur quel planificateur il doit s'exécuter. La chaîne continuera à s'exécuter sur le thread à partir du planificateur spécifié dans Observable#subscribeOn(Scheduler)
jusqu'à ce qu'un appel à Observable#observeOn(Scheduler)
soit effectué avec un autre planificateur. Lorsqu'un tel appel est effectué, tous les observateurs à partir de là (c'est-à-dire les opérations ultérieures en aval de la chaîne) recevront des notifications dans un fil tiré du planificateur observeOn
.
Voici un diagramme en marbre qui montre comment ces méthodes affectent l'endroit où les opérations sont exécutées :
Dans le contexte d'Android, si une opération d'interface utilisateur doit avoir lieu à la suite d'une longue opération, nous voudrions que cette opération ait lieu sur le thread d'interface utilisateur. À cette fin, nous pouvons utiliser AndroidScheduler#mainThread()
, l'un des planificateurs fournis dans la bibliothèque RxAndroid.

RxJava sur Android
Maintenant que nous avons quelques notions de base à notre actif, vous vous demandez peut-être quelle est la meilleure façon d'intégrer RxJava dans une application Android ? Comme vous pouvez l'imaginer, il existe de nombreux cas d'utilisation de RxJava mais, dans cet exemple, examinons un cas spécifique : l'utilisation d'objets observables dans le cadre de la pile réseau.
Dans cet exemple, nous examinerons Retrofit, un client HTTP open source de Square qui a des liaisons intégrées avec RxJava pour interagir avec l'API de GitHub. Plus précisément, nous allons créer une application simple qui présente tous les référentiels étoilés pour un utilisateur doté d'un nom d'utilisateur GitHub. Si vous voulez aller de l'avant, le code source est disponible ici.
Créer un nouveau projet Android
- Commencez par créer un nouveau projet Android et nommez-le GitHubRxJava .
- Dans l'écran Target Android Devices , gardez Phone et Tablet sélectionnés et définissez le niveau SDK minimum de 17. N'hésitez pas à le définir sur un niveau d'API inférieur/supérieur mais, pour cet exemple, le niveau d'API 17 suffira.
- Sélectionnez Activité vide dans l'invite suivante.
- Dans la dernière étape, conservez le nom de l'activité en tant que MainActivity et générez un fichier de mise en page activity_main .
Configuration du projet
Incluez RxJava, RxAndroid et la bibliothèque Retrofit dans app/build.gradle
. Notez que l'inclusion de RxAndroid inclut implicitement également RxJava. Cependant, il est préférable de toujours inclure explicitement ces deux bibliothèques, car RxAndroid ne contient pas toujours la version la plus récente de RxJava. L'inclusion explicite de la dernière version de RxJava garantit l'utilisation de la version la plus récente.
dependencies { compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' compile 'com.squareup.retrofit2:retrofit:2.1.0' compile 'io.reactivex:rxandroid:1.2.0' compile 'io.reactivex:rxjava:1.1.8' // ...other dependencies }
Créer un objet de données
Créez la classe d'objets de données GitHubRepo
. Cette classe encapsule un référentiel dans GitHub (la réponse réseau contient plus de données mais nous ne sommes intéressés que par un sous-ensemble de cela).
public class GitHubRepo { public final int id; public final String name; public final String htmlUrl; public final String description; public final String language; public final int stargazersCount; public GitHubRepo(int id, String name, String htmlUrl, String description, String language, int stargazersCount) { this.id = id; this.name = name; this.htmlUrl = htmlUrl; this.description = description; this.language = language; this.stargazersCount = stargazersCount; } }
Mise à niveau de configuration
- Créez l'interface
GitHubService
. Nous passerons cette interface dans Retrofit et Retrofit créera une implémentation deGitHubService
.
public interface GitHubService { @GET("users/{user}/starred") Observable<List<GitHubRepo>> getStarredRepositories(@Path("user") String userName); }
Créez la classe
GitHubClient
. Ce sera l'objet avec lequel nous interagirons pour effectuer des appels réseau à partir du niveau de l'interface utilisateur.Lors de la construction d'une implémentation de
GitHubService
via Retrofit, nous devons transmettre unRxJavaCallAdapterFactory
en tant qu'adaptateur d'appel afin que les appels réseau puissent renvoyer des objets Observable (le passage d'un adaptateur d'appel est nécessaire pour tout appel réseau qui renvoie un résultat autre qu'unCall
).Nous devons également transmettre une
GsonConverterFactory
afin de pouvoir utiliser Gson comme moyen de marshaler des objets JSON en objets Java.
public class GitHubClient { private static final String GITHUB_BASE_URL = "https://api.github.com/"; private static GitHubClient instance; private GitHubService gitHubService; private GitHubClient() { final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); final Retrofit retrofit = new Retrofit.Builder().baseUrl(GITHUB_BASE_URL) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); gitHubService = retrofit.create(GitHubService.class); } public static GitHubClient getInstance() { if (instance == null) { instance = new GitHubClient(); } return instance; } public Observable<List<GitHubRepo>> getStarredRepos(@NonNull String userName) { return gitHubService.getStarredRepositories(userName); } }
Dispositions de configuration
Ensuite, créez une interface utilisateur simple qui affiche les dépôts récupérés en fonction d'un nom d'utilisateur GitHub d'entrée. Créez activity_home.xml
- la mise en page de notre activité - avec quelque chose comme ceci :
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android: android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <EditText android: android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:hint="@string/username"/> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/search"/> </LinearLayout> </LinearLayout>
Créez item_github_repo.xml
- la disposition de l'élément ListView
pour l'objet de référentiel GitHub - avec quelque chose comme ceci :
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="6dp"> <TextView android: android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="24sp" android:text tools:text="Cropper"/> <TextView android: android:layout_width="match_parent" android:layout_height="wrap_content" android:lines="2" android:ellipsize="end" android:textSize="16sp" android:layout_below="@+id/text_repo_name" tools:text="Android widget for cropping and rotating an image."/> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_repo_description" android:layout_alignParentLeft="true" android:textColor="?attr/colorPrimary" android:textSize="14sp" android:text tools:text="Language: Java"/> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/text_repo_description" android:layout_alignParentRight="true" android:textColor="?attr/colorAccent" android:textSize="14sp" android:text tools:text="Stars: 1953"/> </RelativeLayout>
Collez tout ensemble
Créez un ListAdapter
chargé de lier les objets GitHubRepo
aux éléments ListView
. Le processus consiste essentiellement à gonfler item_github_repo.xml
dans une View
si aucune View
recyclée n'est fournie ; sinon, une View
recyclée est réutilisée pour éviter de trop gonfler trop d'objets View
.
public class GitHubRepoAdapter extends BaseAdapter { private List<GitHubRepo> gitHubRepos = new ArrayList<>(); @Override public int getCount() { return gitHubRepos.size(); } @Override public GitHubRepo getItem(int position) { if (position < 0 || position >= gitHubRepos.size()) { return null; } else { return gitHubRepos.get(position); } } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { final View view = (convertView != null ? convertView : createView(parent)); final GitHubRepoViewHolder viewHolder = (GitHubRepoViewHolder) view.getTag(); viewHolder.setGitHubRepo(getItem(position)); return view; } public void setGitHubRepos(@Nullable List<GitHubRepo> repos) { if (repos == null) { return; } gitHubRepos.clear(); gitHubRepos.addAll(repos); notifyDataSetChanged(); } private View createView(ViewGroup parent) { final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final View view = inflater.inflate(R.layout.item_github_repo, parent, false); final GitHubRepoViewHolder viewHolder = new GitHubRepoViewHolder(view); view.setTag(viewHolder); return view; } private static class GitHubRepoViewHolder { private TextView textRepoName; private TextView textRepoDescription; private TextView textLanguage; private TextView textStars; public GitHubRepoViewHolder(View view) { textRepoName = (TextView) view.findViewById(R.id.text_repo_name); textRepoDescription = (TextView) view.findViewById(R.id.text_repo_description); textLanguage = (TextView) view.findViewById(R.id.text_language); textStars = (TextView) view.findViewById(R.id.text_stars); } public void setGitHubRepo(GitHubRepo gitHubRepo) { textRepoName.setText(gitHubRepo.name); textRepoDescription.setText(gitHubRepo.description); textLanguage.setText("Language: " + gitHubRepo.language); textStars.setText("Stars: " + gitHubRepo.stargazersCount); } } }
Collez le tout ensemble dans MainActivity
. Il s'agit essentiellement de l' Activity
qui s'affiche lorsque nous lançons l'application pour la première fois. Ici, nous demandons à l'utilisateur d'entrer son nom d'utilisateur GitHub, et enfin, d'afficher tous les référentiels étoilés par ce nom d'utilisateur.
public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private GitHubRepoAdapter adapter = new GitHubRepoAdapter(); private Subscription subscription; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ListView listView = (ListView) findViewById(R.id.list_view_repos); listView.setAdapter(adapter); final EditText editTextUsername = (EditText) findViewById(R.id.edit_text_username); final Button buttonSearch = (Button) findViewById(R.id.button_search); buttonSearch.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { final String username = editTextUsername.getText().toString(); if (!TextUtils.isEmpty(username)) { getStarredRepos(username); } } }); } @Override protected void onDestroy() { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } super.onDestroy(); } private void getStarredRepos(String username) { subscription = GitHubClient.getInstance() .getStarredRepos(username) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Observer<List<GitHubRepo>>() { @Override public void onCompleted() { Log.d(TAG, "In onCompleted()"); } @Override public void onError(Throwable e) { e.printStackTrace(); Log.d(TAG, "In onError()"); } @Override public void onNext(List<GitHubRepo> gitHubRepos) { Log.d(TAG, "In onNext()"); adapter.setGitHubRepos(gitHubRepos); } }); } }
Exécutez l'application
L'exécution de l'application doit présenter un écran avec une zone de saisie pour entrer un nom d'utilisateur GitHub. La recherche devrait alors présenter la liste de tous les dépôts favoris.
Conclusion
J'espère que cela servira d'introduction utile à RxJava et d'un aperçu de ses capacités de base. Il y a une tonne de concepts puissants dans RxJava et je vous exhorte à les explorer en creusant plus profondément dans le wiki RxJava bien documenté.
N'hésitez pas à laisser des questions ou des commentaires dans la boîte de commentaires ci-dessous. Vous pouvez également me suivre sur Twitter à @arriolachris où je tweete beaucoup sur RxJava et tout ce qui concerne Android.
Si vous souhaitez une ressource d'apprentissage complète sur RxJava, vous pouvez consulter l'ebook que j'ai écrit avec Angus Huang sur Leanpub.