Introduction à RxJava

1. Vue d’ensemble

Dans cet article, nous allons nous concentrer sur l’utilisation de Reactive Extensions (Rx) en Java pour composer et utiliser des séquences de données.

En un coup d’œil, l’API peut ressembler à Java 8 Streams, mais en réalité, elle est beaucoup plus souple et fluide, ce qui en fait un puissant paradigme de programmation.

Si vous voulez en savoir plus sur RxJava, consultez le lien:/rxjava-backpressure[cette description].

2. Installer

Pour utiliser RxJava dans notre projet Maven , nous devons ajouter la dépendance suivante à notre pom .xml:

<dependency>
    <groupId>io.reactivex</groupId>
    <artifactId>rxjava</artifactId>
    <version>${rx.java.version}</version>
</dependency>

Ou, pour un projet Gradle:

compile 'io.reactivex.rxjava:rxjava:x.y.z'

3. Concepts fonctionnels réactifs

D’un côté, ** functional programming est le processus de création de logiciels en composant des fonctions pures, évitant les états partagés, les données mutables et les effets secondaires.

De l’autre côté, reactive programming est un paradigme de programmation asynchrone qui concerne les flux de données et la propagation du changement.

Ensemble, la programmation réactive fonctionnelle forme une combinaison de techniques fonctionnelles et réactives pouvant représenter une approche élégante de la programmation événementielle, avec des valeurs qui changent avec le temps et où le consommateur réagit aux données à mesure qu’elles arrivent.

Cette technologie regroupe différentes implémentations de ses principes de base. Certains auteurs ont élaboré un document définissant le vocabulaire commun permettant de décrire le nouveau type d’applications.

3.1. Manifeste réactif

Le Reactive Manifesto est un document en ligne qui définit un standard élevé pour les applications du secteur du développement de logiciels. En termes simples, les systèmes réactifs sont:

  • Réactif - les systèmes doivent réagir rapidement

  • Message Driven - les systèmes doivent utiliser la transmission asynchrone des messages entre

composants pour assurer un couplage lâche ** Élastique - les systèmes doivent rester réactifs sous forte charge

  • Résilient - les systèmes doivent rester réactifs lorsque certains composants échouent

4. Observables

Lorsque vous travaillez avec Rx, vous devez comprendre deux types de clé:

  • Observable représente n’importe quel objet pouvant extraire des données d’une donnée.

source et dont l’état peut présenter un intérêt de manière à ce que d’autres objets peut inscrire un intérêt ** Un observer est un objet qui souhaite être notifié lorsque l’état

d’un autre objet change

Un observer s’abonne à une séquence Observable . La séquence envoie des éléments au observer un à un.

Observer gère chacun d’eux avant de traiter le suivant. Si de nombreux événements arrivent de manière asynchrone, ils doivent être stockés dans une file d’attente ou abandonnés.

Dans Rx , un observer ne sera jamais appelé avec un élément en panne ou appelé avant que le rappel ne soit retourné pour l’élément précédent.

4.1. Types de Observable

Il y a deux types:

  • Non-Blocking – L’exécution asynchrone est prise en charge et autorisée à

désinscrire à tout moment dans le flux d’événements. Sur cet article, nous allons nous concentrer principalement sur ce type de type

  • Blocking – tous les appels onNext de l’observateur seront synchrones et

Il n’est pas possible de se désabonner au milieu d’un flux d’événements. On peut toujours convertir un Observable en un Blocking Observable , en utilisant la méthode toBlocking:

BlockingObservable<String> blockingObservable = observable.toBlocking();

4.2. Les opérateurs

  • Un operator est une fonction qui prend un Observable (la source) comme premier argument et renvoie un autre Observable (la destination). ** Ensuite, pour chaque élément émis par la source observable, il appliquera une fonction à cet élément, puis émettra le résultat sur la destination Observable .

Les opérateurs peuvent être chaînés pour créer des flux de données complexes qui filtrent les événements en fonction de certains critères. Plusieurs opérateurs peuvent être appliqués au même observable .

Il n’est pas difficile de se retrouver dans une situation dans laquelle un observateur émet des éléments plus rapidement qu’un opérateur ou un observateur ne peut les consommer.

Vous pouvez en savoir plus sur le lien de back-pressure:/rxjava-backpressure[ici.]

4.3. Créer observable

L’opérateur de base just produit un Observable qui émet une seule instance générique avant la fin, String «Hello» . Lorsque nous voulons extraire des informations d’un Observable , nous implémentons une interface observer , puis appelons subscribe sur le Observable souhaité:

Observable<String> observable = Observable.just("Hello");
observable.subscribe(s -> result = s);

assertTrue(result.equals("Hello"));

4.4. OnNext, OnError, et OnCompleted

Il y a trois méthodes sur l’interface observer que nous voulons connaître:

  1. OnNext est appelé sur notre observer chaque fois qu’un nouvel événement est

publié sur le Observable attaché. C’est la méthode où nous allons effectuer des actions sur chaque événement . OnCompleted est appelée lorsque la séquence d’événements associée à un

Observable est terminé, ce qui indique qu’il ne faut plus en attendre onNext fait appel à notre observateur . OnError est appelée lorsqu’une exception non gérée est levée au cours de la

Code d’infrastructure RxJava ou notre code de gestion d’événement

La valeur de retour pour la méthode Observables subscribe est une interface subscribe :

String[]letters = {"a", "b", "c", "d", "e", "f", "g"};
Observable<String> observable = Observable.from(letters);
observable.subscribe(
  i -> result += i, //OnNext
  Throwable::printStackTrace,//OnError
  () -> result += "__Completed"//OnCompleted
);
assertTrue(result.equals("abcdefg__Completed"));

5. Transformations observables et opérateurs conditionnels

5.1. Carte

M _ap operator transforme les éléments émis par un Observable_ en appliquant une fonction à chaque élément.

Supposons qu’il existe un tableau déclaré de chaînes contenant des lettres de l’alphabet et que nous souhaitons les imprimer en mode majuscule:

Observable.from(letters)
  .map(String::toUpperCase)
  .subscribe(letter -> result += letter);
assertTrue(result.equals("ABCDEFG"));

La flatMap peut être utilisée pour aplatir Observables chaque fois que nous nous retrouvons avec des Observables imbriqués.

Plus de détails sur la différence entre map et flatMap peuvent être trouvés ici .

En supposant que nous ayons une méthode qui retourne un Observable <String> à partir d’une liste de chaînes. Nous allons maintenant imprimer pour chaque chaîne d’un nouveau Observable la liste des titres en fonction de ce que Subscriber voit:

Observable<String> getTitle() {
    return Observable.from(titleList);
}
Observable.just("book1", "book2")
  .flatMap(s -> getTitle())
  .subscribe(l -> result += l);

assertTrue(result.equals("titletitle"));

5.2. Balayage

L’opérateur _scan a applique une fonction à chaque élément émis par un Observable_ de manière séquentielle et émet chaque valeur successive.

Cela nous permet de reporter l’état d’un événement à l’autre:

String[]letters = {"a", "b", "c"};
Observable.from(letters)
  .scan(new StringBuilder(), StringBuilder::append)
  .subscribe(total -> result += total.toString());
assertTrue(result.equals("aababc"));

5.3. Par groupe

Grouper par opérateur nous permet de classer les événements de l’observable d’entrée en catégories de sortie.

Supposons que nous ayons créé un tableau d’entiers compris entre 0 et 10, puis appliquons group by qui les divisera en catégories even et odd :

Observable.from(numbers)
  .groupBy(i -> 0 == (i % 2) ? "EVEN" : "ODD")
  .subscribe(group ->
    group.subscribe((number) -> {
        if (group.getKey().toString().equals("EVEN")) {
            EVEN[0]+= number;
        } else {
            ODD[0]+= number;
        }
    })
  );
assertTrue(EVEN[0].equals("0246810"));
assertTrue(ODD[0].equals("13579"));

5.4. Filtre

L’opérateur filter n’émet que les éléments d’un observable qui passent un test predicate .

Nous allons donc filtrer dans un tableau d’entiers les nombres impairs

Observable.from(numbers)
  .filter(i -> (i % 2 == 1))
  .subscribe(i -> result += i);

assertTrue(result.equals("13579"));

5.5. Opérateurs conditionnels

DefaultIfEmpty émet un élément à partir de la source Observable ou un élément par défaut si la source Observable est vide:

Observable.empty()
  .defaultIfEmpty("Observable is empty")
  .subscribe(s -> result += s);

assertTrue(result.equals("Observable is empty"));

Le code suivant émet la première lettre de l’alphabet ‘ a ’ car le tableau letters n’est pas vide et c’est ce qu’il contient en première position:

Observable.from(letters)
  .defaultIfEmpty("Observable is empty")
  .first()
  .subscribe(s -> result += s);

assertTrue(result.equals("a"));

L’opérateur TakeWhile élimine les éléments émis par un Observable lorsqu’une condition spécifiée devient fausse:

Observable.from(numbers)
  .takeWhile(i -> i < 5)
  .subscribe(s -> sum[0]+= s);

assertTrue(sum[0]== 10);

Bien sûr, il existe plusieurs autres opérateurs pouvant couvrir nos besoins, tels que Contain, SkipWhile, SkipUntil, TakeUntil, etc.

6. Observables connectables

  • Un ConnectableObservable ressemble à un Observable ordinaire, sauf qu’il ne commence pas à émettre d’éléments lorsqu’il est abonné, mais uniquement lorsque l’opérateur connect lui est appliqué.

De cette manière, nous pouvons attendre que tous les observateurs prévus souscrivent à l’Observable__ avant que celui-ci ne commence à émettre des éléments:

String[]result = {""};
ConnectableObservable<Long> connectable
  = Observable.interval(200, TimeUnit.MILLISECONDS).publish();
connectable.subscribe(i -> result[0]+= i);
assertFalse(result[0].equals("01"));

connectable.connect();
Thread.sleep(500);

assertTrue(result[0].equals("01"));

7. Célibataire

Single est comme un Observable qui, au lieu d’émettre une série de valeurs, émet une valeur ou une notification d’erreur.

Avec cette source de données, on ne peut utiliser que deux méthodes pour s’abonner:

  • OnSuccess renvoie un Single qui appelle également une méthode que nous spécifions

  • OnError renvoie également un Single qui avertit immédiatement

abonnés d’une erreur

String[]result = {""};
Single<String> single = Observable.just("Hello")
  .toSingle()
  .doOnSuccess(i -> result[0]+= i)
  .doOnError(error -> {
      throw new RuntimeException(error.getMessage());
  });
single.subscribe();

assertTrue(result[0].equals("Hello"));

8. Sujets

Un objet est simultanément deux éléments, un abonné et un observable. En tant qu’abonné, un sujet peut être utilisé pour publier les événements provenant de plusieurs observateurs.

Et comme il est également observable, les événements de plusieurs abonnés peuvent être retransmis en tant qu’événements à tous ceux qui les observent.

Dans l’exemple suivant, nous verrons comment les observateurs pourront voir les événements qui se produisent après leur inscription

Integer subscriber1 = 0;
Integer subscriber2 = 0;
Observer<Integer> getFirstObserver() {
    return new Observer<Integer>() {
        @Override
        public void onNext(Integer value) {
           subscriber1 += value;
        }

        @Override
        public void onError(Throwable e) {
            System.out.println("error");
        }

        @Override
        public void onCompleted() {
            System.out.println("Subscriber1 completed");
        }
    };
}

Observer<Integer> getSecondObserver() {
    return new Observer<Integer>() {
        @Override
        public void onNext(Integer value) {
            subscriber2 += value;
        }

        @Override
        public void onError(Throwable e) {
            System.out.println("error");
        }

        @Override
        public void onCompleted() {
            System.out.println("Subscriber2 completed");
        }
    };
}

PublishSubject<Integer> subject = PublishSubject.create();
subject.subscribe(getFirstObserver());
subject.onNext(1);
subject.onNext(2);
subject.onNext(3);
subject.subscribe(getSecondObserver());
subject.onNext(4);
subject.onCompleted();

assertTrue(subscriber1 + subscriber2 == 14)

9. La gestion des ressources

L’opération Utilisation nous permet d’associer des ressources, telles qu’une connexion à une base de données JDBC, une connexion réseau ou d’ouvrir des fichiers à nos observables.

Nous présentons dans les commentaires les étapes à suivre pour atteindre cet objectif, ainsi qu’un exemple de mise en œuvre:

String[]result = {""};
Observable<Character> values = Observable.using(
  () -> "MyResource",
  r -> {
      return Observable.create(o -> {
          for (Character c : r.toCharArray()) {
              o.onNext(c);
          }
          o.onCompleted();
      });
  },
  r -> System.out.println("Disposed: " + r)
);
values.subscribe(
  v -> result[0]+= v,
  e -> result[0]+= e
);
assertTrue(result[0].equals("MyResource"));

10. Conclusion

Dans cet article, nous avons parlé de l’utilisation de la bibliothèque RxJava et de l’exploration de ses fonctionnalités les plus importantes.

Le code source complet du projet, y compris tous les exemples de code utilisés ici, est disponible à l’adresse over sur Github .