RxJava Un observable, plusieurs abonnés

1. Vue d’ensemble

Le comportement par défaut de plusieurs abonnés n’est pas toujours souhaitable. Dans cet article, nous expliquerons comment modifier ce comportement et gérer correctement plusieurs abonnés.

Mais voyons d’abord le comportement par défaut de plusieurs abonnés.

2. Comportement par défaut

Disons que nous avons le Observable suivant:

private static Observable getObservable() {
    return Observable.create(subscriber -> {
        subscriber.onNext(gettingValue(1));
        subscriber.onNext(gettingValue(2));

        subscriber.add(Subscriptions.create(() -> {
            LOGGER.info("Clear resources");
        }));
    });
}

Cela émet deux éléments dès que le __Subscriber __s est abonné.

Dans notre exemple, nous avons deux __Subscriber __s:

LOGGER.info("Subscribing");

Subscription s1 = obs.subscribe(i -> LOGGER.info("subscriber#1 is printing " + i));
Subscription s2 = obs.subscribe(i -> LOGGER.info("subscriber#2 is printing " + i));

s1.unsubscribe();
s2.unsubscribe();

Imaginez que l’obtention de chaque élément soit une opération coûteuse - cela peut inclure, par exemple, un calcul intensif ou l’ouverture d’une connexion URL.

Pour que les choses restent simples, nous allons simplement renvoyer un numéro:

private static Integer gettingValue(int i) {
    LOGGER.info("Getting " + i);
    return i;
}

Voici la sortie:

Subscribing
Getting 1
subscriber#1 is printing 1
Getting 2
subscriber#1 is printing 2
Getting 1
subscriber#2 is printing 1
Getting 2
subscriber#2 is printing 2
Clear resources
Clear resources

Comme nous pouvons le voir, obtenir chaque élément et effacer les ressources est effectué deux fois par défaut - une fois pour chaque Abonné. Ce n’est pas ce que nous voulons. La classe ConnectableObservable__ permet de résoudre le problème.

3. ConnectableObservable

La classe ConnectableObservable permet de partager l’abonnement avec plusieurs abonnés et de ne pas effectuer plusieurs fois les opérations sous-jacentes.

Mais commençons par créer un ConnectableObservable .

3.1. publier()

La méthode publish () est ce qui crée un ConnectableObservable à partir d’un Observable :

ConnectableObservable obs = Observable.create(subscriber -> {
    subscriber.onNext(gettingValue(1));
    subscriber.onNext(gettingValue(2));
    subscriber.add(Subscriptions.create(() -> {
        LOGGER.info("Clear resources");
    }));
}).publish();

Mais pour l’instant, ça ne fait rien. La méthode connect () lui permet de fonctionner.

3.2. relier()

  • Jusqu’à ce que la méthode ConnectableObservable ’s connect () ne soit pas appelée Observable ’s onSubcribe () le rappel ne soit pas déclenché ** même s’il existe des abonnés.

Montrons ceci:

LOGGER.info("Subscribing");
obs.subscribe(i -> LOGGER.info("subscriber #1 is printing " + i));
obs.subscribe(i -> LOGGER.info("subscriber #2 is printing " + i));
Thread.sleep(1000);
LOGGER.info("Connecting");
Subscription s = obs.connect();
s.unsubscribe();

Nous nous abonnons puis attendons une seconde avant de nous connecter. La sortie est:

Subscribing
Connecting
Getting 1
subscriber #1 is printing 1
subscriber #2 is printing 1
Getting 2
subscriber #1 is printing 2
subscriber #2 is printing 2
Clear resources

Comme on peut le voir:

  • Obtenir des éléments ne se produit qu’une fois comme nous le souhaitions

  • ** Les ressources de compensation ne se produisent qu’une fois de plus

  • ** L’obtention d’éléments commence une seconde après la souscription.

  • ** L’abonnement ne déclenche plus l’émission d’éléments. Seulement

connect () fait cela

Ce délai peut être bénéfique - nous devons parfois donner à tous les abonnés la même séquence d’éléments même si l’un d’entre eux s’abonne plus tôt qu’un autre.

3.3. La vue cohérente des observables - connect () après subscribe ()

Ce cas d’utilisation ne peut pas être démontré sur notre précédent __Observable car il fait froid et que les deux abonnés obtiennent quand même la séquence complète des éléments.

Imaginons au contraire qu’un élément émetteur ne dépende pas du moment de l’abonnement, des événements émis lors de clics de souris, par exemple. Maintenant, imaginons aussi qu’un second Subscriber s’abonne une seconde après la première.

Le premier Subscriber obtiendra tous les éléments émis lors de cet exemple, tandis que le second Subscriber ne recevra que certains éléments.

D’autre part, utiliser la méthode connect () au bon endroit peut donner aux deux abonnés la même vue sur la séquence Observable .

  • Exemple de Hot Observable **

Créons un Observable chaud. Il émettra des éléments sur des clics de souris sur JFrame .

Chaque élément sera la coordonnée x du clic:

private static Observable getObservable() {
    return Observable.create(subscriber -> {
        frame.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                subscriber.onNext(e.getX());
            }
        });
        subscriber.add(Subscriptions.create(() {
            LOGGER.info("Clear resources");
            for (MouseListener listener : frame.getListeners(MouseListener.class)) {
                frame.removeMouseListener(listener);
            }
        }));
    });
}
  • Le comportement par défaut de Hot Observable **

Maintenant, si nous souscrivons deux _Subscriber s l’un après l’autre avec un second intervalle, exécutons le programme et cliquons dessus, nous verrons que le premier Subscriber_ aura plus d’éléments:

public static void defaultBehaviour() throws InterruptedException {
    Observable obs = getObservable();

    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe((i) ->
        LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe((i) ->
        LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#1");
    subscription1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#2");
    subscription2.unsubscribe();
}
subscribing #1
subscriber#1 is printing x-coordinate 280
subscriber#1 is printing x-coordinate 242
subscribing #2
subscriber#1 is printing x-coordinate 343
subscriber#2 is printing x-coordinate 343
unsubscribe#1
clearing resources
unsubscribe#2
clearing resources
  • connect () après subscribe () **

Pour que les deux abonnés obtiennent la même séquence, nous allons convertir ce Observable en ConnectableObservable et appeler connect () après la souscription avec les deux __Subscriber __s:

public static void subscribeBeforeConnect() throws InterruptedException {

    ConnectableObservable obs = getObservable().publish();

    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe(
      i -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe(
      i ->  LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("connecting:");
    Subscription s = obs.connect();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe connected");
    s.unsubscribe();
}

Maintenant, ils auront la même séquence:

subscribing #1
subscribing #2
connecting:
subscriber#1 is printing x-coordinate 317
subscriber#2 is printing x-coordinate 317
subscriber#1 is printing x-coordinate 364
subscriber#2 is printing x-coordinate 364
unsubscribe connected
clearing resources

Il faut donc attendre le moment où tous les abonnés sont prêts, puis appeler connect () .

Dans une application Spring, nous pouvons par exemple souscrire tous les composants lors du démarrage de l’application et appeler connect () dans onApplicationEvent () .

Mais revenons à notre exemple. notez que tous les clics avant la méthode connect () sont manqués. Si nous ne voulons pas rater d’éléments mais au contraire les traiter, nous pouvons mettre connect () plus tôt dans le code et forcer le Observable à produire des événements en l’absence de Subscriber .

3.4. Forçage de l’abonnement en l’absence de Subscriber - connect () Before subscribe ()

Pour illustrer cela, corrigeons notre exemple:

public static void connectBeforeSubscribe() throws InterruptedException {
    ConnectableObservable obs = getObservable()
      .doOnNext(x -> LOGGER.info("saving " + x)).publish();
    LOGGER.info("connecting:");
    Subscription s = obs.connect();
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    obs.subscribe((i) -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    obs.subscribe((i) -> LOGGER.info("subscriber#2 is printing x-coordinate " + i));
    Thread.sleep(1000);
    s.unsubscribe();
}

Les étapes sont relativement simples:

  • Tout d’abord, nous nous connectons

  • Ensuite, on attend une seconde et on s’abonne au premier Subscriber

  • Enfin, nous attendons une seconde de plus et souscrivons à la seconde

Abonné

Notez que nous avons ajouté l’opérateur doOnNext () . Ici, nous pourrions stocker des éléments dans la base de données par exemple, mais dans notre code, nous imprimons simplement «save …​».

Si nous lançons le code et commençons à cliquer, nous verrons que les éléments sont émis et traités immédiatement après l’appel de connect () :

connecting:
saving 306
saving 248
subscribing #1
saving 377
subscriber#1 is printing x-coordinate 377
saving 295
subscriber#1 is printing x-coordinate 295
saving 206
subscriber#1 is printing x-coordinate 206
subscribing #2
saving 347
subscriber#1 is printing x-coordinate 347
subscriber#2 is printing x-coordinate 347
clearing resources

S’il n’y avait aucun abonné, les éléments seraient toujours traités.

  • Ainsi, la méthode connect () commence à émettre et à traiter des éléments indépendamment du fait que quelqu’un soit abonné ** comme s’il y avait un Subscriber artificiel avec une action vide qui consommait les éléments.

Et si de vrais __Subscriber __s sont abonnés, ce médiateur artificiel ne leur transmet que des éléments.

Pour vous désabonner du Subscriber artificiel, nous effectuons:

s.unsubscribe();

Où:

Subscription s = obs.connect();

3.5. autoConnect ()

  • Cette méthode implique que connect () ne soit pas appelé avant ou après les abonnements, mais automatiquement lorsque le premier Subscriber est abonné ** .

Avec cette méthode, nous ne pouvons pas appeler connect () nous-mêmes car l’objet renvoyé est un Observable habituel qui n’a pas cette méthode mais utilise un ConnectableObservable sous-jacent:

public static void autoConnectAndSubscribe() throws InterruptedException {
    Observable obs = getObservable()
    .doOnNext(x -> LOGGER.info("saving " + x)).publish().autoConnect();

    LOGGER.info("autoconnect()");
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    Subscription s1 = obs.subscribe((i) ->
        LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription s2 = obs.subscribe((i) ->
        LOGGER.info("subscriber#2 is printing x-coordinate " + i));

    Thread.sleep(1000);
    LOGGER.info("unsubscribe 1");
    s1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe 2");
    s2.unsubscribe();
}

Notez que nous ne pouvons pas non plus nous désabonner du Subscriber artificiel. Nous pouvons annuler tous les abonnés réels, mais l’abonné artificiel continuera de traiter les événements.

Pour comprendre cela, regardons ce qui se passe à la fin du désabonnement du dernier abonné:

subscribing #1
saving 296
subscriber#1 is printing x-coordinate 296
saving 329
subscriber#1 is printing x-coordinate 329
subscribing #2
saving 226
subscriber#1 is printing x-coordinate 226
subscriber#2 is printing x-coordinate 226
unsubscribe 1
saving 268
subscriber#2 is printing x-coordinate 268
saving 234
subscriber#2 is printing x-coordinate 234
unsubscribe 2
saving 278
saving 268

Comme nous pouvons le constater, la suppression des ressources n’a pas lieu et la sauvegarde des éléments avec doOnNext () se poursuit après la deuxième désinscription. Cela signifie que le Subscriber artificiel ne se désabonne pas mais continue de consommer des éléments.

3.6. refCount ()

  • refCount () est similaire à autoConnect () en ce sens que la connexion est également automatique dès que le premier Subscriber est abonné. **

Contrairement à autoconnect () , la déconnexion est également automatique lorsque le dernier Subscriber se désabonne:

public static void refCountAndSubscribe() throws InterruptedException {
    Observable obs = getObservable()
      .doOnNext(x -> LOGGER.info("saving " + x)).publish().refCount();

    LOGGER.info("refcount()");
    Thread.sleep(1000);
    LOGGER.info("subscribing #1");
    Subscription subscription1 = obs.subscribe(
      i -> LOGGER.info("subscriber#1 is printing x-coordinate " + i));
    Thread.sleep(1000);
    LOGGER.info("subscribing #2");
    Subscription subscription2 = obs.subscribe(
      i -> LOGGER.info("subscriber#2 is printing x-coordinate " + i));

    Thread.sleep(1000);
    LOGGER.info("unsubscribe#1");
    subscription1.unsubscribe();
    Thread.sleep(1000);
    LOGGER.info("unsubscribe#2");
    subscription2.unsubscribe();
}
refcount()
subscribing #1
saving 265
subscriber#1 is printing x-coordinate 265
saving 338
subscriber#1 is printing x-coordinate 338
subscribing #2
saving 203
subscriber#1 is printing x-coordinate 203
subscriber#2 is printing x-coordinate 203
unsubscribe#1
saving 294
subscriber#2 is printing x-coordinate 294
unsubscribe#2
clearing resources

4. Conclusion

La classe ConnectableObservable permet de gérer plusieurs abonnés avec un minimum d’effort.

Ses méthodes semblent similaires mais modifient considérablement le comportement des abonnés en raison des subtilités de l’implémentation. Même l’ordre des méthodes est important.

Le code source complet de tous les exemples utilisés dans cet article se trouve dans le projet GitHub .