Quand faut-il utiliser RxJava Observable et quand un simple rappel sur Android?

260

Je travaille sur la mise en réseau de mon application. J'ai donc décidé d'essayer Retrofit Square . Je vois qu'ils prennent en charge simpleCallback

@GET("/user/{id}/photo")
void getUserPhoto(@Path("id") int id, Callback<Photo> cb);

et RxJava Observable

@GET("/user/{id}/photo")
Observable<Photo> getUserPhoto(@Path("id") int id);

Les deux semblent assez similaires à première vue, mais quand il s'agit de la mise en œuvre, cela devient intéressant ...

Alors qu'avec une implémentation de rappel simple, cela ressemblerait à ceci:

api.getUserPhoto(photoId, new Callback<Photo>() {
    @Override
    public void onSuccess() {
    }
});

ce qui est assez simple et direct. Et avec Observablecela devient rapidement verbeux et assez compliqué.

public Observable<Photo> getUserPhoto(final int photoId) {
    return Observable.create(new Observable.OnSubscribeFunc<Photo>() {
        @Override
        public Subscription onSubscribe(Observer<? super Photo> observer) {
            try {
                observer.onNext(api.getUserPhoto(photoId));
                observer.onCompleted();
            } catch (Exception e) {
                observer.onError(e);
            }

            return Subscriptions.empty();
        }
    }).subscribeOn(Schedulers.threadPoolForIO());
}

Et ce n'est pas ça. Vous devez encore faire quelque chose comme ça:

Observable.from(photoIdArray)
        .mapMany(new Func1<String, Observable<Photo>>() {
            @Override
            public Observable<Photo> call(Integer s) {
                return getUserPhoto(s);
            }
        })
        .subscribeOn(Schedulers.threadPoolForIO())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<Photo>() {
            @Override
            public void call(Photo photo) {
                //save photo?
            }
        });

Est-ce que j'ai râté quelque chose? Ou est-ce un mauvais cas pour utiliser Observables? Quand préfèrerait-on / devrait-on préférer un Observablesimple rappel?

Mettre à jour

L'utilisation de retrofit est beaucoup plus simple que l'exemple ci-dessus comme @Niels l'a montré dans sa réponse ou dans l'exemple de projet U2020 de Jake Wharton . Mais la question reste essentiellement la même: quand utiliser l'un ou l'autre?

Martynas Jurkus
la source
pouvez-vous mettre à jour votre lien vers le fichier dont vous parlez dans U2020
letroll
Ça marche toujours ...
Martynas Jurkus
4
J'ai eu les mêmes pensées exactement quand je lisais RxJava était la nouvelle chose. J'ai lu un exemple de mise à niveau (parce que je le connais très bien) d'une simple demande et c'était dix ou quinze lignes de code et ma première réaction a été que tu te moquais de moi = /. Je ne peux pas non plus comprendre comment cela remplace un bus d'événements, car le bus d'événements vous dissocie de l'observable et rxjava réintroduit le couplage, sauf erreur de ma part.
Lo-Tan

Réponses:

348

Pour les choses de réseautage simples, les avantages de RxJava par rapport à Callback sont très limités. Le simple exemple getUserPhoto:

RxJava:

api.getUserPhoto(photoId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Photo>() {
            @Override
            public void call(Photo photo) {
               // do some stuff with your photo 
            }
     });

Rappeler:

api.getUserPhoto(photoId, new Callback<Photo>() {
    @Override
    public void onSuccess(Photo photo, Response response) {
    }
});

La variante RxJava n'est pas beaucoup mieux que la variante Callback. Pour l'instant, ignorons la gestion des erreurs. Prenons une liste de photos:

RxJava:

api.getUserPhotos(userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<List<Photo>, Observable<Photo>>() {
    @Override
    public Observable<Photo> call(List<Photo> photos) {
         return Observable.from(photos);
    }
})
.filter(new Func1<Photo, Boolean>() {
    @Override
    public Boolean call(Photo photo) {
         return photo.isPNG();
    }
})
.subscribe(
    new Action1<Photo>() {
    @Override
        public void call(Photo photo) {
            list.add(photo)
        }
    });

Rappeler:

api.getUserPhotos(userId, new Callback<List<Photo>>() {
    @Override
    public void onSuccess(List<Photo> photos, Response response) {
        List<Photo> filteredPhotos = new ArrayList<Photo>();
        for(Photo photo: photos) {
            if(photo.isPNG()) {
                filteredList.add(photo);
            }
        }
    }
});

Maintenant, la variante RxJava n'est toujours pas plus petite, bien qu'avec Lambdas, elle serait plus proche de la variante Callback. De plus, si vous avez accès au flux JSON, il serait assez étrange de récupérer toutes les photos lorsque vous n'affichez que les fichiers PNG. Ajustez simplement le flux pour qu'il affiche uniquement les fichiers PNG.

Première conclusion

Cela ne rend pas votre base de code plus petite lorsque vous chargez un simple JSON que vous avez préparé pour être au bon format.

Maintenant, rendons les choses un peu plus intéressantes. Supposons que vous souhaitiez non seulement récupérer la photo utilisateur, mais que vous ayez un clone Instagram et que vous souhaitiez récupérer 2 JSON: 1. getUserDetails () 2. getUserPhotos ()

Vous souhaitez charger ces deux JSON en parallèle, et lorsque les deux sont chargés, la page doit être affichée. La variante de rappel deviendra un peu plus difficile: vous devez créer 2 rappels, stocker les données dans l'activité, et si toutes les données sont chargées, afficher la page:

Rappeler:

api.getUserDetails(userId, new Callback<UserDetails>() {
    @Override
    public void onSuccess(UserDetails details, Response response) {
        this.details = details;
        if(this.photos != null) {
            displayPage();
        }
    }
});

api.getUserPhotos(userId, new Callback<List<Photo>>() {
    @Override
    public void onSuccess(List<Photo> photos, Response response) {
        this.photos = photos;
        if(this.details != null) {
            displayPage();
        }
    }
});

RxJava:

private class Combined {
    UserDetails details;
    List<Photo> photos;
}


Observable.zip(api.getUserDetails(userId), api.getUserPhotos(userId), new Func2<UserDetails, List<Photo>, Combined>() {
            @Override
            public Combined call(UserDetails details, List<Photo> photos) {
                Combined r = new Combined();
                r.details = details;
                r.photos = photos;
                return r;
            }
        }).subscribe(new Action1<Combined>() {
            @Override
            public void call(Combined combined) {
            }
        });

Nous arrivons quelque part! Le code de RxJava est maintenant aussi gros que l'option de rappel. Le code RxJava est plus robuste; Pensez à ce qui se passerait si nous avions besoin d'un troisième JSON à charger (comme les dernières vidéos)? Le RxJava n'aurait besoin que d'un petit ajustement, tandis que la variante de rappel doit être ajustée à plusieurs endroits (à chaque rappel, nous devons vérifier si toutes les données sont récupérées).

Un autre exemple; nous voulons créer un champ de saisie semi-automatique, qui charge les données à l'aide de Retrofit. Nous ne voulons pas faire un appel Web chaque fois qu'un EditText a un TextChangedEvent. Lorsque vous tapez rapidement, seul le dernier élément doit déclencher l'appel. Sur RxJava, nous pouvons utiliser l'opérateur anti-rebond:

inputObservable.debounce(1, TimeUnit.SECONDS).subscribe(new Action1<String>() {
            @Override
            public void call(String s) {
                // use Retrofit to create autocompletedata
            }
        });

Je ne vais pas créer la variante Callback mais vous comprendrez que c'est beaucoup plus de travail.

Conclusion: RxJava est exceptionnellement bon lorsque les données sont envoyées sous forme de flux. Le Retrofit Observable pousse tous les éléments du flux en même temps. Ce n'est pas particulièrement utile en soi par rapport à Callback. Mais quand il y a plusieurs éléments poussés sur le flux et à différents moments, et que vous devez faire des choses liées au timing, RxJava rend le code beaucoup plus maintenable.

Niels
la source
6
Quelle classe avez-vous utilisée pour le inputObservable? J'ai beaucoup de problèmes avec le rebond et j'aimerais en savoir un peu plus sur cette solution.
Migore
Le projet @Migore RxBinding a le module "Platform binding" qui fournit les classes comme RxView, RxTextViewetc. qui peuvent être utilisées pour le inputObservable.
blizzard
@Niels pouvez-vous expliquer comment ajouter la gestion des erreurs? pouvez-vous créer un abonné avec onCompleted, onError et onNext si vous flatMap, Filter et ensuite vous abonner? Merci beaucoup pour votre grande explication.
Nicolas Jafelle
5
Ceci est un formidable exemple!
Ye Lin Aung
3
Meilleure explication jamais!
Denis Nek
66

Le truc observable est déjà fait dans Retrofit, donc le code pourrait être le suivant:

api.getUserPhoto(photoId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Photo>() {
         @Override
            public void call(Photo photo) {
                //save photo?
            }
     });
Niels
la source
5
Oui, mais la question est de savoir quand préférer l'un à l'autre?
Christopher Perry
7
Alors, comment est-ce mieux qu'un simple rappel?
Martynas Jurkus
14
Les observables @MartynasJurkus peuvent être utiles si vous souhaitez chaîner plusieurs fonctions. Par exemple, l'appelant souhaite obtenir une photo recadrée aux dimensions 100x100. L'API peut renvoyer n'importe quelle photo de taille, vous pouvez donc mapper le getUserPhoto observable à un autre ResizedPhotoObservable - l'appelant n'est informé que lorsque le redimensionnement est terminé. Si vous n'avez pas besoin de l'utiliser, ne le forcez pas.
ataulm
2
Un commentaire mineur. Il n'est pas nécessaire d'appeler .subscribeOn (Schedulers.io ()) car RetroFit s'en occupe déjà - github.com/square/retrofit/issues/430 (voir la réponse de Jake)
hiBrianLee
4
@MartynasJurkus en plus des choses dites par ataulm, contrairement à Callbackvous pouvez vous désinscrire Observeable, évitant ainsi les problèmes de cycle de vie courants.
Dmitry Zaytsev du
35

Dans le cas de getUserPhoto (), les avantages de RxJava ne sont pas importants. Mais prenons un autre exemple lorsque vous obtiendrez toutes les photos d'un utilisateur, mais uniquement lorsque l'image est au format PNG et que vous n'avez pas accès au JSON pour effectuer le filtrage côté serveur.

api.getUserPhotos(userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<List<Photo>, Observable<Photo>>() {
    @Override
    public Observable<Photo> call(List<Photo> photos) {
         return Observable.from(photos);
    }
})
.filter(new Func1<Photo, Boolean>() {
    @Override
    public Boolean call(Photo photo) {
         return photo.isPNG();
    }
})
.subscribe(
    new Action1<Photo>() {
    @Override
        public void call(Photo photo) {
            // on main thread; callback for each photo, add them to a list or something.
            list.add(photo)
        }
    }, 
    new Action1<Throwable>() {
    @Override
        public void call(Throwable throwable) {
            // on main thread; something went wrong
            System.out.println("Error! " + throwable);
        }
    }, 
    new Action0() {
        @Override
        public void call() {
            // on main thread; all photo's loaded, time to show the list or something.
        }
    });

Maintenant, le JSON retourne une liste de photos. Nous allons les mapper à des éléments individuels. Ce faisant, nous pourrons utiliser la méthode de filtrage pour ignorer les photos qui ne sont pas PNG. Après cela, nous nous abonnerons et recevrons un rappel pour chaque photo individuelle, un gestionnaire d'erreur et un rappel lorsque toutes les lignes seront terminées.

TLDR Point ici étant; le rappel ne vous renvoie qu'un rappel de réussite et d'échec; l'Observable RxJava vous permet de cartographier, réduire, filtrer et bien d'autres choses.

Niels
la source
Tout d'abord, subscribeOn et observeOn ne sont pas nécessaires avec le retrofit. Il fera l'appel réseau de manière asynchrone et notifiera sur le même thread que l'appelant. Lorsque vous ajoutez également RetroLambda pour obtenir des expressions lambda, cela devient beaucoup plus agréable.
mais je peux le faire (l'image du filtre est PNG) dans le .subscribe () Pourquoi devrais-je utiliser le filtre? et il n'y a pas besoin de flatmap
SERG
3
3 réponses de @Niels. Le premier répond à la question. Aucun avantage pour le 2e
Raymond Chenon
même réponse que la première
Mayank Sharma
27

Avec rxjava, vous pouvez faire plus de choses avec moins de code.

Supposons que vous souhaitiez implémenter la recherche instantanée dans votre application. Avec les rappels, vous vous inquiétez de vous désabonner de la demande précédente et de vous abonner à la nouvelle, de gérer vous-même le changement d'orientation ... Je pense que c'est beaucoup de code et trop verbeux.

Avec rxjava, c'est très simple.

public class PhotoModel{
  BehaviorSubject<Observable<Photo>> subject = BehaviorSubject.create(...);

  public void setUserId(String id){
   subject.onNext(Api.getUserPhoto(photoId));
  }

  public Observable<Photo> subscribeToPhoto(){
    return Observable.switchOnNext(subject);
  }
}

si vous voulez implémenter la recherche instantanée, il vous suffit d'écouter TextChangeListener et d'appeler photoModel.setUserId(EditText.getText());

Dans la méthode onCreate de Fragment ou d'activité, vous vous abonnez à l'Observable qui renvoie photoModel.subscribeToPhoto (), il retourne un Observable qui émet toujours les éléments émis par le dernier Observable (demande).

AndroidObservable.bindFragment(this, photoModel.subscribeToPhoto())
                 .subscribe(new Action1<Photo>(Photo photo){
      //Here you always receive the response of the latest query to the server.
                  });

De plus, si PhotoModel est un Singleton, par exemple, vous n'avez pas à vous soucier des changements d'orientation, car BehaviorSubject émet la dernière réponse du serveur, quel que soit le moment de votre abonnement.

Avec ces lignes de code, nous avons implémenté une recherche instantanée et géré les changements d'orientation. Pensez-vous que vous pouvez implémenter cela avec des rappels avec moins de code? J'en doute.

Roger Garzon Nieto
la source
Ne pensez-vous pas que faire de la classe PhotoModel un singleton est en soi une restriction? Imaginez que ce n'est pas un profil utilisateur mais un ensemble de photos pour plusieurs utilisateurs, n'obtiendrons-nous que la dernière photo utilisateur demandée? De plus, demander des données sur chaque changement d'orientation me semble un peu choquant, qu'en pensez-vous?
Farid
2

Nous suivons généralement la logique suivante:

  1. S'il s'agit d'un simple appel à réponse unique, alors Callback ou Future est préférable.
  2. S'il s'agit d'un appel avec plusieurs réponses (flux), ou lorsqu'il existe une interaction complexe entre différents appels (voir la réponse de @Niels ), alors les observables sont meilleurs.
Dzmitry Lazerka
la source
1

D'après les échantillons et les conclusions des autres réponses, je pense qu'il n'y a pas de grande différence pour les tâches simples en une ou deux étapes. Cependant, le rappel est simple et direct. RxJava est plus compliqué et trop grand pour une tâche simple. Il y a la troisième solution par: AbacusUtil . Permettez-moi d'implémenter les cas d'utilisation ci-dessus avec les trois solutions: Callback, RxJava, CompletableFuture (AbacusUtil) avec Retrolambda :

Récupérer la photo du réseau et enregistrer / afficher sur l'appareil:

// By Callback
api.getUserPhoto(userId, new Callback<Photo>() {
    @Override
    public void onResponse(Call<Photo> call, Response<Photo> response) {
        save(response.body()); // or update view on UI thread.
    }

    @Override
    public void onFailure(Call<Photo> call, Throwable t) {
        // show error message on UI or do something else.
    }
});

// By RxJava
api.getUserPhoto2(userId) //
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(photo -> {
            save(photo); // or update view on UI thread.
        }, error -> {
            // show error message on UI or do something else.
        });

// By Thread pool executor and CompletableFuture.
TPExecutor.execute(() -> api.getUserPhoto(userId))
        .thenRunOnUI((photo, error) -> {
            if (error != null) {
                // show error message on UI or do something else.
            } else {
                save(photo); // or update view on UI thread.
            }
        });

Charger les détails de l'utilisateur et la photo en parallèle

// By Callback
// ignored because it's little complicated

// By RxJava
Observable.zip(api.getUserDetails2(userId), api.getUserPhoto2(userId), (details, photo) -> Pair.of(details, photo))
        .subscribe(p -> {
            // Do your task.
        });

// By Thread pool executor and CompletableFuture.
TPExecutor.execute(() -> api.getUserDetails(userId))
          .runOnUIAfterBoth(TPExecutor.execute(() -> api.getUserPhoto(userId)), p -> {
    // Do your task
});
user_3380739
la source
4
OP ne demandait pas de suggestions pour une nouvelle bibliothèque
forresthopkinsa
1

Personnellement, je préfère utiliser Rx pour obtenir des réponses api dans les cas où je dois filtrer, mapper ou quelque chose comme ça sur les données Ou dans les cas où je dois faire un autre appel api en fonction des réponses aux appels précédents

Reza
la source
0

Il semble que vous réinventiez la roue, ce que vous faites est déjà implémenté dans la rénovation.

Par exemple, vous pouvez regarder RestAdapterTest.java de retrofit , où ils définissent une interface avec Observable comme type de retour, puis l' utiliser .

Samuel Gruetter
la source
Merci d'avoir répondu. Je vois ce que vous dites, mais je ne sais pas encore comment je mettrais cela en œuvre. Pourriez-vous fournir un exemple simple en utilisant l'implémentation de mise à niveau existante afin que je puisse vous attribuer la prime?
Yehosef
@Yehosef quelqu'un a-t-il supprimé son commentaire ou vous faisiez semblant d'être OP? ))
Farid
Vous pouvez attribuer une prime sur la question de quelqu'un d'autre. Quand j'ai posé cette question, j'avais vraiment besoin de la réponse à la question - alors j'ai offert une prime. Si vous planez sur le prix de la prime, vous verrez qu'il a été décerné par moi.
Yehosef
0

Lorsque vous créez une application pour le plaisir, un projet pour animaux de compagnie ou un POC ou le 1er prototype, vous utilisez des classes Android / Java simples comme les rappels, les tâches asynchrones, les boucles, les threads, etc. Elles sont simples à utiliser et ne nécessitent aucune Intégration de bibliothèques tierces. Une grande intégration de bibliothèque juste pour construire un petit projet immuable est illogique quand quelque chose de similaire peut être fait dès le départ.

Cependant, ce sont comme un couteau très tranchant. Les utiliser dans un environnement de production est toujours cool, mais cela aura aussi des conséquences. Il est difficile d'écrire du code simultané sûr si vous n'êtes pas bien familiarisé avec le codage propre et les principes SOLIDES. Vous devrez maintenir une architecture appropriée pour faciliter les changements futurs et améliorer la productivité de l'équipe.

D'autre part, les bibliothèques de concurrence comme RxJava, Co-routines, etc. sont essayées et testées plus d'un milliard de fois pour aider à écrire du code concurrent prêt pour la production. Encore une fois, ce n'est pas qu'en utilisant ces bibliothèques, vous n'écrivez pas de code simultané ou n'abstenez pas toute la logique de concurrence. Vous l'êtes toujours. Mais maintenant, il est visible et applique un modèle clair pour l'écriture de code simultané dans toute la base de code et, plus important encore, dans votre équipe de développement.

Il s'agit d'un avantage majeur de l'utilisation d'un cadre de concurrence au lieu des anciennes classes de base simples qui traitent de la concurrence brute. Cependant, ne vous méprenez pas. Je suis un grand partisan de la limitation des dépendances de la bibliothèque externe, mais dans ce cas spécifique, vous devrez créer un cadre personnalisé pour votre base de code qui est une tâche longue et ne peut être effectuée qu'après une expérience préalable. Par conséquent, les cadres de concurrence sont préférés à l'utilisation de classes simples comme les rappels, etc.


TL'DR

Si vous utilisez déjà RxJava pour le codage simultané dans toute la base de code, utilisez simplement RxJava Observable / Flowable. Une sage question à poser est alors de savoir si j'utilise Observables à Flowables. Sinon, allez-y et utilisez un appelable.

Sagar Gupta
la source