Kiedy należy korzystać z RxJava Observable, a kiedy proste Callback na Androidzie?

260

Pracuję nad siecią dla mojej aplikacji. Postanowiłem więc wypróbować Square's Retrofit . Widzę, że obsługują prosteCallback

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

i RxJava Observable

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

Oba wyglądają dość podobnie na pierwszy rzut oka, ale kiedy dochodzi do implementacji, robi się interesująco ...

Podczas gdy z prostą implementacją wywołania zwrotnego wyglądałoby podobnie do tego:

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

co jest dość proste i jednoznaczne. A dzięki Observabletemu szybko staje się gadatliwy i dość skomplikowany.

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

I to nie wszystko. Nadal musisz zrobić coś takiego:

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

Czy coś mi umyka? Czy jest to niewłaściwy przypadek użycia Observables? Kiedy należy / należy preferować Observableproste wywołanie zwrotne?

Aktualizacja

Korzystanie z modernizacji jest znacznie prostsze niż powyższy przykład, jak pokazał @Niels w swojej odpowiedzi lub w przykładowym projekcie Jake'a Whartona U2020 . Ale w gruncie rzeczy pytanie pozostaje takie samo - kiedy należy użyć jednego lub drugiego sposobu?

Martynas Jurkus
źródło
czy możesz zaktualizować link do pliku, o którym mówisz w U2020
letroll
Nadal działa ...
Martynas Jurkus
4
Człowieku, miałem te same myśli dokładnie wtedy, gdy czytałem RxJava, było czymś nowym. Przeczytałem przykład modernizacji (ponieważ jestem bardzo dobrze zaznajomiony) z prostym żądaniem i było to dziesięć lub piętnaście linii kodu, a moją pierwszą reakcją było to, że żartujesz sobie ze mnie = /. Nie mogę również dowiedzieć się, jak to zastępuje magistralę zdarzeń, ponieważ magistrala zdarzeń oddziela cię od obserwowalnego i rxjava przywraca sprzężenie, chyba że się mylę.
Lo-Tan,

Odpowiedzi:

348

W przypadku prostych rzeczy sieciowych przewaga RxJava nad wywołaniem zwrotnym jest bardzo ograniczona. Prosty przykład getUserPhoto:

RxJava:

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

Oddzwonić:

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

Wariant RxJava nie jest dużo lepszy niż wariant Callback. Na razie zignorujmy obsługę błędów. Zróbmy listę zdjęć:

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

Oddzwonić:

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

Teraz wariant RxJava wciąż nie jest mniejszy, chociaż w przypadku Lambdas byłby on coraz bliżej wariantu Callback. Ponadto, jeśli masz dostęp do kanału JSON, byłoby dziwnie odzyskać wszystkie zdjęcia, gdy wyświetlasz tylko pliki PNG. Wystarczy dostosować kanał, aby wyświetlał tylko pliki PNG.

Pierwszy wniosek

Nie zmniejsza to bazy kodu, gdy ładujesz prosty JSON, który przygotowałeś do właściwego formatu.

Teraz sprawmy, by było trochę ciekawiej. Powiedzmy, że nie tylko chcesz odzyskać zdjęcie użytkownika, ale masz klon na Instagramie i chcesz pobrać 2 JSON: 1. getUserDetails () 2. getUserPhotos ()

Chcesz załadować te dwa JSON równolegle, a gdy oba zostaną załadowane, strona powinna zostać wyświetlona. Wariant wywołania zwrotnego stanie się nieco trudniejszy: musisz utworzyć 2 połączenia zwrotne, zapisać dane w działaniu, a jeśli wszystkie dane są załadowane, wyświetl stronę:

Oddzwonić:

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

Dostajemy gdzieś! Kod RxJava jest teraz tak duży, jak opcja wywołania zwrotnego. Kod RxJava jest bardziej niezawodny; Pomyśl, co by się stało, gdybyśmy potrzebowali załadować trzeciego JSON (jak najnowsze filmy)? RxJava wymagałby tylko niewielkiej korekty, podczas gdy wariant oddzwaniania musi być dostosowany w wielu miejscach (przy każdym oddzwanianiu musimy sprawdzić, czy wszystkie dane są pobierane).

Inny przykład; chcemy utworzyć pole autouzupełniania, które ładuje dane za pomocą Retrofit. Nie chcemy robić połączenia za każdym razem, gdy EditText ma TextChangedEvent. Podczas szybkiego pisania tylko ostatni element powinien wyzwalać połączenie. Na RxJava możemy użyć operatora debounce:

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

Nie stworzę wariantu oddzwaniania, ale zrozumiecie, że to dużo więcej pracy.

Wniosek: RxJava jest wyjątkowo dobry, gdy dane są wysyłane jako strumień. Retrofit Observable popycha wszystkie elementy w strumieniu w tym samym czasie. To nie jest szczególnie przydatne samo w sobie w porównaniu do oddzwaniania. Ale kiedy w strumieniu jest wiele elementów wypychanych i w różnych czasach, i musisz robić rzeczy związane z taktowaniem, RxJava sprawia, że ​​kod jest o wiele łatwiejszy w utrzymaniu.

Niels
źródło
6
Z jakiej klasy korzystałeś inputObservable? Mam wiele problemów z ogłaszaniem i chciałbym dowiedzieć się nieco więcej o tym rozwiązaniu.
Migore
@Migore RxBinding projekt ma moduł „Platforma wiążące”, które zapewnia zajęcia jak RxView, RxTextViewetc, które mogą być wykorzystane do inputObservable.
zamieć
@Niels czy możesz wyjaśnić, jak dodać obsługę błędów? czy można utworzyć subskrybenta za pomocą onCompleted, onError i onNext, jeśli flatMap, filtruj, a następnie subskrybujesz? Dziękuję bardzo za wielkie wyjaśnienie.
Nicolas Jafelle,
4
To wspaniały przykład!
Ye Lin Aung
3
Najlepsze wyjaśnienie w historii!
Denis Nek,
66

Obserwowalne rzeczy są już zrobione w ramach Retrofit, więc kod może wyglądać tak:

api.getUserPhoto(photoId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Photo>() {
         @Override
            public void call(Photo photo) {
                //save photo?
            }
     });
Niels
źródło
5
Tak, ale pytanie brzmi: kiedy preferować jedno od drugiego?
Christopher Perry
7
Więc jak to jest lepsze niż zwykłe oddzwanianie?
Martynas Jurkus
14
Obserwowalne @MartynasJurkus mogą być przydatne, jeśli chcesz połączyć kilka funkcji. Na przykład dzwoniący chce uzyskać zdjęcie przycięte do wymiarów 100 x 100. Interfejs API może zwrócić zdjęcie w dowolnym rozmiarze, dzięki czemu można zmapować getUserPhoto obserwowalny na inny ResizedPhotoObservable - dzwoniący otrzyma powiadomienie dopiero po zakończeniu zmiany rozmiaru. Jeśli nie musisz go używać, nie zmuszaj go.
ataulm
2
Jedna drobna informacja zwrotna. Nie trzeba wywoływać .subscribeOn (Schedulers.io ()), ponieważ RetroFit już się tym zajmuje - github.com/square/retrofit/issues/430 (patrz odpowiedź Jake'a)
hiBrianLee
4
@MartynasJurkus oprócz rzeczy wypowiedzianych przez ataulm, w przeciwieństwie do tych, z Callbackktórych można zrezygnować Observeable, unikając typowych problemów w cyklu życia.
Dmitrij Zajcew
35

W przypadku getUserPhoto () zalety RxJava nie są świetne. Ale weźmy inny przykład, gdy dostaniesz wszystkie zdjęcia dla użytkownika, ale tylko wtedy, gdy obraz jest w formacie PNG i nie masz dostępu do JSON, aby wykonać filtrowanie po stronie serwera.

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

Teraz JSON zwraca listę zdjęć. Spłaszczymy je do poszczególnych elementów. W ten sposób będziemy mogli użyć metody filtrowania, aby zignorować zdjęcia, które nie są PNG. Następnie zasubskrybujemy i otrzymamy oddzwonienie dla każdego pojedynczego zdjęcia, moduł obsługi błędów i oddzwonienie, gdy wszystkie wiersze zostaną zakończone.

Punktem tutaj jest TLDR ; oddzwonienie zwraca tylko oddzwonienie w przypadku sukcesu i niepowodzenia; RxJava Observable pozwala robić mapy, redukować, filtrować i wiele innych rzeczy.

Niels
źródło
Po pierwsze subskrybuj i obserwuj Nie są konieczne przy modernizacji. Wykona połączenie sieciowe asynchronicznie i powiadomi w tym samym wątku co osoba dzwoniąca. Gdy dodasz również RetroLambda, aby uzyskać wyrażenia lambda, staje się znacznie ładniejsza.
ale mogę to zrobić (obraz filtra to PNG) w .subscribe () Dlaczego powinienem używać filtra? i nie ma potrzeby w flatmap
SERG
3
3 odpowiedzi od @Niels. Pierwszy odpowiada na pytanie. Brak korzyści dla 2.
Raymond Chenon
taka sama odpowiedź jak pierwsza
Mayank Sharma
27

Z rxjava możesz robić więcej rzeczy przy mniejszym kodzie.

Załóżmy, że chcesz wdrożyć natychmiastowe wyszukiwanie w swojej aplikacji. W przypadku wywołań zwrotnych martwisz się o rezygnację z subskrypcji poprzedniej prośby i zasubskrybowanie nowej, samodzielna obsługa orientacji ... Myślę, że to dużo kodu i jest zbyt szczegółowy.

Z rxjava jest bardzo prosta.

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

jeśli chcesz wdrożyć natychmiastowe wyszukiwanie, musisz tylko nasłuchiwać TextChangeListener i wywoływać photoModel.setUserId(EditText.getText());

W metodzie fragmentu lub aktywności onCreate subskrybujesz Observable, która zwraca photoModel.subscribeToPhoto (), zwraca Observable, które zawsze emituje elementy emitowane przez najnowszą Observable (żądanie).

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

Ponadto, jeśli PhotoModel jest na przykład Singletonem, nie musisz się martwić o zmiany orientacji, ponieważ BehaviorSubject emituje ostatnią odpowiedź serwera, niezależnie od tego, kiedy subskrybujesz.

Dzięki tym wierszom kodu zaimplementowaliśmy natychmiastowe wyszukiwanie i obsługujemy zmiany orientacji. Czy uważasz, że możesz to zaimplementować za pomocą wywołań zwrotnych z mniejszym kodem? Wątpię.

Roger Garzon Nieto
źródło
Czy nie uważasz, że uczynienie klasy PhotoModel singletonem samo w sobie jest ograniczeniem? Wyobraź sobie, że nie jest to profil użytkownika, ale zestaw zdjęć dla wielu użytkowników, czy nie otrzymamy tylko ostatnio żądanego zdjęcia użytkownika? Poza tym prośba o dane dotyczące każdej zmiany orientacji wydaje mi się trochę dziwna, co myślisz?
Farid
2

Zwykle stosujemy następującą logikę:

  1. Jeśli jest to zwykłe połączenie z jedną odpowiedzią, wtedy Callback lub Future jest lepszy.
  2. Jeśli jest to połączenie z wieloma odpowiedziami (strumień) lub gdy zachodzi złożona interakcja między różnymi połączeniami (patrz odpowiedź @Niels ), to Obserwowalne są lepsze.
Dzmitry Lazerka
źródło
1

Na podstawie próbek i wniosków z innych odpowiedzi uważam, że nie ma dużej różnicy dla prostych zadań jedno- lub dwuetapowych. Oddzwonienie jest jednak proste i jednoznaczne. RxJava jest bardziej skomplikowany i zbyt duży, by wykonać proste zadanie. Istnieje trzecie rozwiązanie: AbacusUtil . Pozwól mi wdrożyć powyższe przypadki użycia ze wszystkimi trzema rozwiązaniami: Callback, RxJava, CompletableFuture (AbacusUtil) z Retrolambda :

Pobierz zdjęcie z sieci i zapisz / wyświetl na urządzeniu:

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

Załaduj dane użytkownika i zdjęcie równolegle

// 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
});
użytkownik_3380739
źródło
4
OP nie
prosiło
1

Ja osobiście wolę używać Rx, aby uzyskać odpowiedzi interfejsu API w przypadkach, w których muszę wykonać filtr, mapę lub coś takiego na danych Lub w przypadkach, gdy muszę wykonać inne wywołania interfejsu API na podstawie odpowiedzi na poprzednie połączenia

Reza
źródło
0

Wygląda na to, że zmieniasz kierownicę, a to, co robisz, zostało już zaimplementowane w ramach modernizacji.

Na przykład, możesz spojrzeć na RestAdapterTest.java modernizacji , gdzie definiują interfejs z Observable jako typem powrotu, a następnie go użyć .

Samuel Gruetter
źródło
Dziękuję za odpowiedź. Rozumiem, co mówisz, ale nie jestem jeszcze pewien, jak to zrealizuję. Czy możesz podać prosty przykład, korzystając z istniejącej implementacji modernizacji, dzięki czemu mogę przyznać nagrodę?
Yehosef
@Yehosef czy ktoś usunął swój komentarz lub udawałeś, że jesteś OP? ))
Farid
Możesz przyznać nagrodę za pytanie kogoś innego. Kiedy o to zapytałem, naprawdę potrzebowałem odpowiedzi na pytanie - więc zaoferowałem nagrodę. Jeśli najedziesz kursorem na nagrodę za nagrodę, zobaczysz, że została ona przyznana przeze mnie.
Yehosef
0

Podczas tworzenia aplikacji dla zabawy, projektu zwierzaka, POC lub pierwszego prototypu korzystasz z prostych podstawowych klas Androida / Java, takich jak wywołania zwrotne, zadania asynchroniczne, pętle, wątki itp. Są one łatwe w użyciu i nie wymagają żadnych Integracja lib stron trzecich. Duża integracja bibliotek tylko po to, by zbudować niewielki, niezmienny projekt, jest nielogiczna, gdy coś podobnego można zrobić od razu.

Są jednak jak bardzo ostry nóż. Używanie ich w środowisku produkcyjnym jest zawsze fajne, ale także będą miały konsekwencje. Trudno jest pisać bezpieczny współbieżny kod, jeśli nie znasz dobrze zasad Czystego Kodowania i zasad SOLID. Musisz zachować odpowiednią architekturę, aby ułatwić przyszłe zmiany i poprawić wydajność zespołu.

Z drugiej strony biblioteki współbieżności, takie jak RxJava, wspólne procedury itp. Są testowane i testowane ponad miliard razy, aby pomóc w pisaniu gotowego do produkcji współbieżnego kodu. Znowu nie jest tak, że przy użyciu tych bibliotek nie piszesz współbieżnego kodu ani nie wyodrębniasz całej logiki współbieżności. Nadal jesteś. Ale teraz jest widoczny i wymusza wyraźny wzorzec do pisania współbieżnego kodu w całej bazie kodu, a co ważniejsze w całym zespole programistycznym.

Jest to główna zaleta korzystania z frameworku współbieżności zamiast zwykłych starych klas podstawowych, które zajmują się surową współbieżnością. Nie zrozumcie mnie jednak źle. Jestem wielkim zwolennikiem ograniczenia zależności biblioteki zewnętrznej, ale w tym konkretnym przypadku będziesz musiał zbudować niestandardową strukturę dla swojej bazy kodu, co jest czasochłonnym zadaniem i może być wykonane tylko po wcześniejszym doświadczeniu. Dlatego frameworki współbieżności są bardziej preferowane niż używanie zwykłych klas, takich jak wywołania zwrotne itp.


TL'DR

Jeśli już używasz RxJava do współbieżnego kodowania w całej bazie kodu, po prostu użyj RxJava Observable / Flowable. Mądre pytanie, które należy zadać, to czy powinienem użyć Observables to Flowables. Jeśli nie, to idź dalej i użyj opcji na żądanie.

Sagar Gupta
źródło