web-dev-qa-db-fra.com

Observable unique avec plusieurs abonnés

J'ai une Observable<<List<Foo>> getFoo() créée à partir d'un service de mise à niveau et après avoir appelé la méthode .getFoo(), je dois la partager avec plusieurs abonnés. Cependant, l'appel de la méthode .share() entraîne la réexécution de l'appel réseau. L'opérateur de relecture ne fonctionne pas non plus. Je sais qu'une solution potentielle pourrait être .cache(), mais je ne sais pas pourquoi ce comportement est provoqué.

// Create an instance of our GitHub API interface.
Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(API_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
            .build();

// Create a call instance for looking up Retrofit contributors.
Observable<List<Contributor>> testObservable = retrofit
        .create(GitHub.class)
        .contributors("square", "retrofit")
        .share();

Subscription subscription1 = testObservable
       .subscribe(new Subscriber<List<Contributor>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onNext(List<Contributor> contributors) {
                System.out.println(contributors);
            }
         });

Subscription subscription2 = testObservable
        .subscribe(new Subscriber<List<Contributor>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onNext(List<Contributor> contributors) {
                System.out.println(contributors + " -> 2");
            }
         });

subscription1.unsubscribe();
subscription2.unsubscribe();

Le code ci-dessus peut reproduire le comportement susmentionné. Vous pouvez le déboguer et voir que les listes reçues appartiennent à une autre adresse mémoire.

J'ai également considéré ConnectableObservables comme une solution potentielle, mais cela nécessite que je transporte l'original observable et que j'appelle .connect() chaque fois que je veux ajouter un nouvel abonné.

Ce type de comportement avec la fonction .share() fonctionnait correctement jusqu'à Retrofit 1.9. Il a cessé de fonctionner sur Retrofit 2 - beta. Je ne l'ai pas encore testé avec la version Release de Retrofit 2, sortie il y a quelques heures.

EDIT: 01/02/2017

Pour les futurs lecteurs, j'ai écrit un article ici expliquant plus sur le cas!

26
Pavlos

Vous semblez (implicitement) transposer votre ConnectedObservable retourné par .share() dans un Observable normal. Vous voudrez peut-être lire la différence entre les observables chauds et froids.

Essayer

ConnectedObservable<List<Contributor>> testObservable = retrofit
        .create(GitHub.class)
        .contributors("square", "retrofit")
        .share();

Subscription subscription1 = testObservable
   .subscribe(new Subscriber<List<Contributor>>() {
    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable throwable) {

    }

    @Override
    public void onNext(List<Contributor> contributors) {
        System.out.println(contributors);
    }
});

Subscription subscription2 = testObservable
        .subscribe(new Subscriber<List<Contributor>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable throwable) {

            }

            @Override
            public void onNext(List<Contributor> contributors) {
                System.out.println(contributors + " -> 2");
            }
        });

testObservable.connect();
subscription1.unsubscribe();
subscription2.unsubscribe();

Edit: Vous n'avez pas besoin d'appeler connect() chaque fois que vous voulez un nouvel abonnement, vous n'en avez besoin que pour démarrer l'observable. Je suppose que vous pouvez utiliser replay() pour vous assurer que tous les abonnés suivants obtiennent tous les éléments produits

ConnectedObservable<List<Contributor>> testObservable = retrofit
        .create(GitHub.class)
        .contributors("square", "retrofit")
        .share()
        .replay()
26
JohnWowUs

Après avoir vérifié avec le développeur RxJava Dávid Karnok, je voudrais proposer une explication complète de ce qui se passait ici.

share() est définie comme publish().refCount(), i. e. la source Observable est d'abord transformée en ConnectableObservable par publish() mais au lieu d'avoir à appeler connect() "manuellement" cette partie est gérée par refCount(). En particulier, refCount appellera connect() sur le ConnectableObservable lorsqu'il recevra lui-même le premier abonnement; ensuite, tant qu'il y aura au moins un abonné, il restera abonné; et, enfin, lorsque le nombre d'abonnés tombe à 0, il se désabonne vers le haut. Avec froidObservables, comme ceux retournés par Retrofit, cela arrêtera tous les calculs en cours.

Si, après l'un de ces cycles, un autre abonné arrive, refCount appellera à nouveau connect et déclenchera ainsi un nouvel abonnement à l'Observable source. Dans ce cas, cela déclenchera une autre demande de réseau.

Maintenant, cela n'est généralement pas devenu apparent avec Retrofit 1 (et en fait n'importe quelle version avant ce commit ), car ces anciennes versions de Retrofit par défaut déplaçaient toutes les demandes réseau vers un autre thread. Cela signifiait généralement que tous vos appels subscribe() se produiraient pendant que la première requête/Observable était toujours en cours d'exécution et donc les nouveaux Subscribers seraient simplement ajoutés au refCount et ne déclencherait donc pas de requêtes supplémentaires/Observables.

Cependant, les versions plus récentes de Retrofit ne déplacent plus le travail par défaut vers un autre thread - vous devez le faire explicitement en appelant, par exemple, subscribeOn(Schedulers.io()). Si vous ne le faites pas, tout restera simplement sur le thread actuel, ce qui signifie que la deuxième subscribe() ne se produira qu'après que le premier Observable aura appelé onCompleted et donc après tout Subscribers s'est désabonné et tout est arrêté. Maintenant, comme nous l'avons vu dans le premier paragraphe, lorsque la deuxième subscribe() est appelée, share() n'a d'autre choix que de provoquer un autre Subscription à la source Observable et d'en déclencher un autre demande de réseau.

Donc, pour revenir au comportement auquel vous êtes habitué à partir de Retrofit 1, ajoutez simplement subscribeOn(Schedulers.io()).

Cela devrait entraîner l'exécution de la seule requête réseau - la plupart du temps. En principe, cependant, vous pouvez toujours obtenir plusieurs demandes (et vous pouvez toujours le faire avec Retrofit 1), mais uniquement si vos demandes réseau sont extrêmement rapides et/ou que les appels subscribe() se produisent avec un retard considérable, de sorte que, à nouveau, la première requête est terminée lorsque la deuxième subscribe() se produit.

Par conséquent, Dávid suggère d'utiliser cache() (mais il présente les inconvénients que vous avez mentionnés) ou replay().autoConnect(). Selon ces notes de version , autoConnect fonctionne comme seulement la première moitié de refCount, ou plus précisément, il est

similaire à refCount (), sauf qu'il ne se déconnecte pas lorsque les abonnés sont perdus.

Cela signifie que la demande ne sera déclenchée que lorsque la première subscribe() se produira, mais que tous les Subscriber ultérieurs recevront tous les éléments émis, qu'il y ait, à tout moment entre 0, 0 abonnés.

39
david.mihola