Wann sollte man RxJava Observable verwenden und wann einfach Callback auf Android?

260

Ich arbeite am Networking für meine App. Also habe ich beschlossen, Square's Retrofit auszuprobieren . Ich sehe, dass sie einfach unterstützenCallback

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

und RxJava Observable

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

Beide sehen auf den ersten Blick ziemlich ähnlich aus, aber wenn es um die Implementierung geht, wird es interessant ...

Während mit einfachen Rückrufimplementierung ähnlich aussehen würde:

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

Das ist ganz einfach und unkompliziert. Und damit wird Observablees schnell ausführlich und ziemlich kompliziert.

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());
}

Und das ist es nicht. Sie müssen noch so etwas tun:

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?
            }
        });

Vermisse ich hier etwas? Oder ist dies ein falscher Fall, um Observables zu verwenden ? Wann würde / sollte man einen Observableeinfachen Rückruf vorziehen ?

Aktualisieren

Die Nachrüstung ist viel einfacher als im obigen Beispiel, wie @Niels in seiner Antwort oder in Jake Whartons Beispielprojekt U2020 gezeigt hat . Aber im Wesentlichen bleibt die Frage dieselbe - wann sollte man die eine oder andere Art verwenden?

Martynas Jurkus
quelle
können Sie Ihren Link zu der Datei aktualisieren, über die Sie in U2020 sprechen
letroll
Es funktioniert immer noch ...
Martynas Jurkus
4
Mann, ich hatte genau die gleichen Gedanken, als ich RxJava las, war das Neue. Ich habe ein Nachrüstbeispiel (weil ich damit sehr vertraut bin) einer einfachen Anfrage gelesen und es waren zehn oder fünfzehn Codezeilen, und meine erste Reaktion war, dass Sie mich veräppeln müssen = /. Ich kann auch nicht herausfinden, wie dies einen Ereignisbus ersetzt, da der Ereignisbus Sie vom Beobachtbaren entkoppelt und rxjava die Kopplung wieder einführt, es sei denn, ich irre mich.
Lo-Tan

Antworten:

348

Für einfache Netzwerkaufgaben sind die Vorteile von RxJava gegenüber Callback sehr begrenzt. Das einfache getUserPhoto-Beispiel:

RxJava:

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

Zurückrufen:

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

Die RxJava-Variante ist nicht viel besser als die Callback-Variante. Lassen Sie uns vorerst die Fehlerbehandlung ignorieren. Machen wir eine Liste mit Fotos:

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)
        }
    });

Zurückrufen:

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);
            }
        }
    }
});

Jetzt ist die RxJava-Variante immer noch nicht kleiner, obwohl sie mit Lambdas der Callback-Variante näher kommen würde. Wenn Sie Zugriff auf den JSON-Feed haben, ist es außerdem seltsam, alle Fotos abzurufen, wenn Sie nur die PNGs anzeigen. Passen Sie einfach den Feed an, um nur PNGs anzuzeigen.

Erste Schlussfolgerung

Dadurch wird Ihre Codebasis nicht kleiner, wenn Sie einen einfachen JSON laden, den Sie für das richtige Format vorbereitet haben.

Lassen Sie uns die Dinge jetzt etwas interessanter machen. Angenommen, Sie möchten nicht nur das userPhoto abrufen, sondern haben auch einen Instagram-Klon und möchten 2 JSONs abrufen: 1. getUserDetails () 2. getUserPhotos ()

Sie möchten diese beiden JSONs parallel laden. Wenn beide geladen sind, sollte die Seite angezeigt werden. Die Rückrufvariante wird etwas schwieriger: Sie müssen 2 Rückrufe erstellen, die Daten in der Aktivität speichern und, wenn alle Daten geladen sind, die Seite anzeigen:

Zurückrufen:

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) {
            }
        });

Wir kommen irgendwohin! Der Code von RxJava ist jetzt so groß wie die Rückrufoption. Der RxJava-Code ist robuster. Überlegen Sie, was passieren würde, wenn ein dritter JSON geladen werden müsste (wie die neuesten Videos)? Der RxJava müsste nur geringfügig angepasst werden, während die Rückrufvariante an mehreren Stellen angepasst werden muss (bei jedem Rückruf müssen wir überprüfen, ob alle Daten abgerufen wurden).

Ein anderes Beispiel; Wir möchten ein Autocomplete-Feld erstellen, das Daten mit Retrofit lädt. Wir möchten nicht jedes Mal einen Webcall durchführen, wenn ein EditText ein TextChangedEvent hat. Bei schneller Eingabe sollte nur das letzte Element den Aufruf auslösen. Auf RxJava können wir den Entprellungsoperator verwenden:

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

Ich werde die Callback-Variante nicht erstellen, aber Sie werden verstehen, dass dies viel mehr Arbeit ist.

Fazit: RxJava ist außergewöhnlich gut, wenn Daten als Stream gesendet werden. Das Retrofit Observable überträgt alle Elemente im Stream gleichzeitig. Dies ist an sich im Vergleich zu Callback nicht besonders nützlich. Wenn jedoch mehrere Elemente zu unterschiedlichen Zeiten in den Stream verschoben werden und Sie zeitbezogene Aufgaben ausführen müssen, macht RxJava den Code viel wartbarer.

Niels
quelle
6
Welche Klasse hast du für die benutzt inputObservable? Ich habe viele Probleme mit dem Entprellen und möchte etwas mehr über diese Lösung erfahren.
Migore
@Migore RxBinding Projekt hat den „Platform - Bindung“ Modul, das die Klassen wie liefert RxView, RxTextViewetc. , die für die verwendet werden können inputObservable.
Schneesturm
@Niels kannst du erklären, wie man Fehlerbehandlung hinzufügt? Können Sie mit onCompleted, onError und onNext einen Abonnenten erstellen, wenn Sie flatMap, Filter und dann abonnieren? Vielen Dank für Ihre große Erklärung.
Nicolas Jafelle
4
Dies ist ein großartiges Beispiel!
Ye Lin Aung
3
Beste Erklärung aller Zeiten!
Denis Nek
66

Das Observable-Zeug ist bereits in Retrofit erledigt, daher könnte der Code folgendermaßen aussehen:

api.getUserPhoto(photoId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Photo>() {
         @Override
            public void call(Photo photo) {
                //save photo?
            }
     });
Niels
quelle
5
Ja, aber die Frage ist, wann man das eine dem anderen vorziehen soll?
Christopher Perry
7
Wie ist das besser als ein einfacher Rückruf?
Martynas Jurkus
14
@MartynasJurkus Observables können nützlich sein, wenn Sie mehrere Funktionen verketten möchten. Der Anrufer möchte beispielsweise ein Foto erhalten, das auf die Abmessungen 100 x 100 zugeschnitten ist. Die API kann ein Foto beliebiger Größe zurückgeben, sodass Sie das beobachtbare getUserPhoto einem anderen ResizedPhotoObservable zuordnen können. Der Anrufer wird erst benachrichtigt, wenn die Größenänderung abgeschlossen ist. Wenn Sie es nicht benötigen, erzwingen Sie es nicht.
Ataulm
2
Ein kleines Feedback. Es ist nicht erforderlich, .subscribeOn (Schedulers.io ()) aufzurufen, da RetroFit sich bereits darum kümmert - github.com/square/retrofit/issues/430 (siehe Jakes Antwort)
hiBrianLee
4
@MartynasJurkus zusätzlich zu Sachen, die von ataulm gesagt wurden, im Gegensatz zu denen CallbackSie sich abmelden können Observeable, um häufige Probleme im Lebenszyklus zu vermeiden.
Dmitry Zaytsev
35

Im Fall von getUserPhoto () sind die Vorteile für RxJava nicht groß. Nehmen wir ein anderes Beispiel, wenn Sie alle Fotos für einen Benutzer erhalten, jedoch nur, wenn das Bild PNG ist und Sie keinen Zugriff auf den JSON haben, um die Filterung auf der Serverseite durchzuführen.

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.
        }
    });

Jetzt gibt der JSON eine Liste der Fotos zurück. Wir ordnen sie einzelnen Elementen zu. Auf diese Weise können wir die Filtermethode verwenden, um Fotos zu ignorieren, die nicht PNG sind. Danach abonnieren wir und erhalten für jedes einzelne Foto einen Rückruf, einen errorHandler und einen Rückruf, wenn alle Zeilen abgeschlossen sind.

TLDR Punkt hier ist; Der Rückruf gibt Ihnen nur einen Rückruf für Erfolg und Misserfolg zurück. Mit dem RxJava Observable können Sie Karten erstellen, verkleinern, filtern und vieles mehr.

Niels
quelle
Zunächst einmal sind SubscribeOn und ObservOn bei der Nachrüstung nicht erforderlich. Der Netzwerkanruf wird asynchron ausgeführt und im selben Thread wie der Anrufer benachrichtigt. Wenn Sie auch RetroLambda hinzufügen, um Lambda-Ausdrücke zu erhalten, wird es viel schöner.
aber ich kann es tun (Filterbild ist PNG) in der .subscribe () Warum sollte ich Filter verwenden? und es gibt keine Notwendigkeit in flatmap
SERG
3
3 Antworten von @Niels. Der erste beantwortet die Frage. Kein Vorteil für den 2.
Raymond Chenon
gleiche Antwort wie zuerst
Mayank Sharma
27

Mit rxjava können Sie mehr mit weniger Code erledigen.

Nehmen wir an, Sie möchten die Sofort-Suche in Ihrer App implementieren. Bei Rückrufen haben Sie sich Sorgen gemacht, die vorherige Anfrage abzubestellen und die neue zu abonnieren, die Orientierungsänderung selbst zu handhaben ... Ich denke, es ist viel Code und zu ausführlich.

Mit rxjava ist das sehr einfach.

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);
  }
}

Wenn Sie die sofortige Suche implementieren möchten, müssen Sie nur auf TextChangeListener warten und aufrufen photoModel.setUserId(EditText.getText());

In der onCreate-Methode für Fragmente oder Aktivitäten abonnieren Sie das Observable, das photoModel.subscribeToPhoto () zurückgibt. Es gibt ein Observable zurück, das immer die vom neuesten Observable (Anfrage) ausgegebenen Elemente ausgibt.

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

Wenn PhotoModel beispielsweise ein Singleton ist, müssen Sie sich keine Gedanken über Orientierungsänderungen machen, da BehaviorSubject die letzte Serverantwort ausgibt, unabhängig davon, wann Sie sich anmelden.

Mit diesen Codezeilen haben wir eine sofortige Suche implementiert und Orientierungsänderungen behandelt. Denken Sie, dass Sie dies mit Rückrufen mit weniger Code implementieren können? Ich bezweifle das.

Roger Garzon Nieto
quelle
Denken Sie nicht, dass es selbst eine Einschränkung ist, die PhotoModel-Klasse zu einem Singleton zu machen? Stellen Sie sich vor, es handelt sich nicht um ein Benutzerprofil, sondern um eine Reihe von Fotos für mehrere Benutzer. Erhalten wir nicht nur das zuletzt angeforderte Benutzerfoto? Außerdem klingt es für mich ein bisschen unangenehm, bei jeder Orientierungsänderung Daten anzufordern. Was denkst du?
Farid
2

Wir gehen normalerweise mit der folgenden Logik vor:

  1. Wenn es sich um einen einfachen Anruf mit einer Antwort handelt, ist Callback oder Future besser.
  2. Wenn es sich um einen Anruf mit mehreren Antworten (Stream) handelt oder wenn zwischen verschiedenen Anrufen eine komplexe Interaktion besteht (siehe Antwort von @Niels ), sind Observables besser.
Dzmitry Lazerka
quelle
1

Aufgrund der Beispiele und Schlussfolgerungen in anderen Antworten gibt es meines Erachtens keinen großen Unterschied für einfache Aufgaben in einem oder zwei Schritten. Callback ist jedoch einfach und unkompliziert. RxJava ist komplizierter und zu groß für einfache Aufgaben. Es gibt die dritte Lösung von: AbacusUtil . Lassen Sie mich die obigen Anwendungsfälle mit allen drei Lösungen implementieren: Callback, RxJava, CompletableFuture (AbacusUtil) mit Retrolambda :

Foto aus dem Netzwerk holen und auf dem Gerät speichern / anzeigen:

// 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.
            }
        });

Laden Sie Benutzerdetails und Foto parallel

// 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
quelle
4
OP bat nicht um Vorschläge für eine neue Bibliothek
forresthopkinsa
1

Ich persönlich bevorzuge die Verwendung von Rx, um API-Antworten zu erhalten, wenn ich Filter, Map oder ähnliches für die Daten durchführen muss oder wenn ich andere API-Anrufe basierend auf früheren Anrufantworten ausführen muss

Reza
quelle
0

Es sieht so aus, als würden Sie das Rad neu erfinden. Was Sie tun, ist bereits in der Nachrüstung implementiert.

Als Beispiel könnten Sie sich RestAdapterTest.java von retrofit ansehen , wo sie eine Schnittstelle mit Observable als Rückgabetyp definieren und diese dann verwenden .

Samuel Gruetter
quelle
Danke für die Antwort. Ich verstehe, was Sie sagen, bin mir aber noch nicht sicher, wie ich das umsetzen würde. Könnten Sie ein einfaches Beispiel für die vorhandene Nachrüstimplementierung geben, damit ich Ihnen das Kopfgeld gewähren kann?
Yehosef
@Yehosef hat jemand seinen Kommentar gelöscht oder Sie gaben vor, OP zu sein? ))
Farid
Sie können ein Kopfgeld für die Frage eines anderen vergeben. Als ich das fragte, brauchte ich wirklich die Antwort auf die Frage - also bot ich ein Kopfgeld an. Wenn Sie auf dem Kopfgeldpreis schweben, werden Sie sehen, dass er von mir vergeben wurde.
Yehosef
0

Wenn Sie eine App zum Spaß, ein Haustierprojekt oder einen POC oder den ersten Prototyp erstellen, verwenden Sie einfache Kernklassen für Android / Java wie Rückrufe, asynchrone Aufgaben, Loopers, Threads usw. Sie sind einfach zu verwenden und erfordern keine Lib-Integration von Drittanbietern. Eine große Bibliotheksintegration, nur um ein kleines, unveränderliches Projekt zu erstellen, ist unlogisch, wenn etwas Ähnliches von Anfang an getan werden kann.

Diese sind jedoch wie ein sehr scharfes Messer. Die Verwendung in einer Produktionsumgebung ist immer cool, hat aber auch Konsequenzen. Es ist schwierig, sicheren gleichzeitigen Code zu schreiben, wenn Sie mit Clean Coding und SOLID-Prinzipien nicht vertraut sind. Sie müssen eine ordnungsgemäße Architektur beibehalten, um zukünftige Änderungen zu erleichtern und die Teamproduktivität zu verbessern.

Auf der anderen Seite werden Parallelitätsbibliotheken wie RxJava, Co-Routinen usw. über eine Milliarde Mal erprobt und getestet, um produktionsbereiten gleichzeitigen Code zu schreiben. Auch hier ist es nicht so, dass Sie mit diesen Bibliotheken nicht gleichzeitig Code schreiben oder die gesamte Parallelitätslogik abstrahieren. Du bist immer noch. Aber jetzt ist es sichtbar und erzwingt ein klares Muster für das Schreiben von gleichzeitigem Code in der gesamten Codebasis und vor allem im gesamten Entwicklungsteam.

Dies ist ein Hauptvorteil der Verwendung eines Parallelitäts-Frameworks anstelle der einfachen alten Kernklassen, die sich mit roher Parallelität befassen. Versteht mich jedoch nicht falsch. Ich bin fest davon überzeugt, die Abhängigkeiten der externen Bibliothek zu begrenzen, aber in diesem speziellen Fall müssen Sie ein benutzerdefiniertes Framework für Ihre Codebasis erstellen. Dies ist eine zeitaufwändige Aufgabe und kann nur nach vorheriger Erfahrung ausgeführt werden. Daher werden Parallelitäts-Frameworks der Verwendung einfacher Klassen wie Rückrufe usw. vorgezogen.


TL'DR

Wenn Sie RxJava bereits für die gleichzeitige Codierung in der gesamten Codebasis verwenden, verwenden Sie einfach RxJava Observable / Flowable. Eine kluge Frage ist dann, ob ich Observables to Flowables verwenden soll. Wenn nicht, verwenden Sie einen Callable.

Sagar Gupta
quelle