Kiedy należy używać RxJava Observable i kiedy proste Callback na Androida?

Pracuję nad siecią dla mojej aplikacji. Postanowiłem więc wypróbować modernizację Square . Widzę, że wspierają proste Callback

@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 na pierwszy rzut oka wyglądają bardzo podobnie, ale gdy dojdzie do implementacji, robi się ciekawie...

Podczas gdy przy prostej implementacji wywołania zwrotnego wyglądałoby to podobnie:

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

Co jest dość proste i proste. I z Observable szybko staje się gadatliwy i całkiem skomplikowane.

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 to. 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 umknęło? A może to zły przypadek, aby użyć Observable s? Kiedy / kiedy należy preferować Observable zamiast prostego wywołania zwrotnego?

Update

Użycie retrofit jest znacznie prostsze niż przykład powyżej, jak pokazał @Niels w swojej odpowiedzi lub w przykładowym projekcie Jake ' a Whartona U2020 . Ale zasadniczo pytanie pozostaje takie samo-kiedy należy użyć jednej drogi lub inne?

Author: Mike Laren, 2014-02-19

7 answers

Dla prostych rzeczy sieciowych, zalety RxJava nad Callback 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 
            }
     });

Callback:

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

Wariant RxJava nie jest dużo lepszy od wariantu zwrotnego. 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)
        }
    });

Callback:

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 nadal nie jest mniejszy, choć z Lambda byłoby coraz bliżej wariantu zwrotnego. Co więcej, jeśli masz dostęp do kanału JSON, byłoby trochę dziwnie pobierać wszystkie zdjęcia, gdy wyświetlasz tylko PNGs. Wystarczy dostosować kanał do niego wyświetla tylko PNGs.

Pierwszy wniosek

To nie sprawia, że Twoja baza kodowa jest mniejsza, gdy ładujesz prosty JSON, który przygotowałeś do odpowiedniego formatu.

Sprawmy, żeby było ciekawiej. Powiedzmy, że nie tylko chcesz aby odzyskać userPhoto, ale masz Instagram-clone, i chcesz odzyskać 2 JSONs: 1. getUserDetails() 2. getUserPhotos ()

Chcesz załadować te dwa Jsony równolegle, a gdy oba zostaną załadowane, strona powinna zostać wyświetlona. Wariant wywołania zwrotnego stanie się nieco trudniejszy: musisz utworzyć 2 wywołania zwrotne, przechowywać dane w aktywności, a jeśli wszystkie dane są załadowane, wyświetl Strona:

Callback:

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

Do czegoś zmierzamy! Kod RxJava jest teraz tak duży, jak opcja callback. Kod RxJava jest bardziej wytrzymały; Pomyśl, co by się stało, gdybyśmy potrzebowali trzeciego JSONA do załadowania(jak najnowsze filmy)? RxJava potrzebuje tylko niewielkiej korekty, podczas gdy wariant Callback musi być regulowany w wielu miejscach (na każdym callback 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ć webcall za każdym razem, gdy EditText ma TextChangedEvent. Podczas szybkiego pisania tylko ostatni element powinien wywołać 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 będę tworzyć wariant Callback, ale zrozumiesz, że jest to o wiele więcej pracy.

Wniosek: RxJava jest wyjątkowo dobra, gdy dane są przesyłane jako strumień. Doposażenie we wszystkie elementy w strumieniu w tym samym czasie. Nie jest to szczególnie przydatne samo w sobie w porównaniu do Callback. Ale kiedy w strumieniu jest wiele elementów i różne czasy, i musisz zrobić rzeczy związane z czasem, RxJava sprawia, że kod jest o wiele łatwiejszy do utrzymania.

 319
Author: Niels,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-16 10:13:00

Obserwowalne rzeczy są już zrobione w Retrofit, więc kod może być taki:

api.getUserPhoto(photoId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Photo>() {
         @Override
            public void call(Photo photo) {
                //save photo?
            }
     });
 61
Author: Niels,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-21 20:27:40

W przypadku getUserPhoto() korzyści dla RxJava nie są wielkie. Ale weźmy inny przykład, gdy dostaniesz wszystkie zdjęcia dla użytkownika, ale tylko wtedy, gdy obraz jest PNG, a nie masz dostępu do JSON, aby zrobić filtrowanie na serwerze.

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ęć. będziemy flatMap 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 SUBSKRYBUJEMY i otrzymujemy oddzwonienie do każde pojedyncze zdjęcie, errorHandler i wywołanie zwrotne po zakończeniu wszystkich wierszy.

TLDR Punkt tutaj jest; zwrotne tylko zwraca ci zwrotne za sukces i porażkę; RxJava Observable pozwala zrobić mapę, zmniejszyć, filtr i wiele innych rzeczy.

 33
Author: Niels,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-07 09:34:26

Z rxjava możesz zrobić więcej rzeczy z mniejszą ilością kodu.

Załóżmy, że chcesz zaimplementować natychmiastowe wyszukiwanie w aplikacji. W przypadku połączeń zwrotnych, które martwią Cię o anulowanie poprzedniej prośby i subskrypcję nowej, zajmij się zmianą orientacji samodzielnie... Myślę, że to dużo kodu i zbyt gadatliwy.

Z rxjava jest bardzo proste.

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 zaimplementować instant search, wystarczy posłuchać TextChangeListener i zadzwonić do photoModel.setUserId(EditText.getText());

W onCreate metoda fragmentu lub aktywności subskrybujesz obserwowalny, który zwraca fotomodelę.subscribeToPhoto (), zwraca Obserwowalną, która zawsze emituje elementy emitowane przez najnowszą Obserwowalną (request).

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 na przykład PhotoModel jest Singletonem, nie musisz się martwić o zmiany orientacji, ponieważ BehaviorSubject emituje ostatnią odpowiedź serwera, niezależnie od tego, kiedy subskrybujesz.

Dzięki tym liniom kodu zaimplementowaliśmy natychmiastowe wyszukiwanie i obsługa zmian orientacji. Czy uważasz, że możesz to zaimplementować za pomocą wywołań zwrotnych z mniejszą ilością kodu? Wątpię.

 26
Author: Roger Garzon Nieto,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-14 18:41:13

Wygląda na to, że odkrywasz koło na nowo, to, co robisz, jest już zaimplementowane w modernizacji.

Na przykład możesz spojrzeć na Retrofit ' s RestAdapterTest.java, Gdzie definiują interfejs z typem return, a następnie używają go.

 0
Author: Samuel Gruetter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-26 22:23:29

Przez próbki i wnioski w innych odpowiedziach, myślę, że nie ma dużej różnicy dla prostych zadań jedno-lub dwuetapowych. Jednak oddzwanianie jest proste i proste. RxJava jest bardziej skomplikowana i zbyt duża na proste zadanie. Jest trzecie rozwiązanie przez: AbacusUtil . Pozwolę sobie zaimplementować 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ądzenie:

// 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
});
 0
Author: user_3380739,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-07 15:18:54

Zazwyczaj kierujemy się następującą logiką:

  1. jeśli jest to proste połączenie jednej odpowiedzi, to Callback lub przyszłość jest lepsza.
  2. jeśli jest to wywołanie z wieloma odpowiedziami (stream), lub gdy istnieją złożone interakcje między różnymi wywołaniami (zobacz odpowiedź @Niels' ), wtedy Obiekty obserwacyjne są lepsze.
 0
Author: Dzmitry Lazerka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-28 19:29:35