Quando si dovrebbe usare RxJava Observable e quando Callback semplice su Android?


260

Sto lavorando al networking per la mia app. Così ho deciso di provare Retrofit di Square . Vedo che supportano sempliceCallback

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

e di RxJava Observable

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

Entrambi sembrano abbastanza simili a prima vista, ma quando si arriva all'implementazione diventa interessante ...

Mentre con la semplice implementazione di callback sarebbe simile a questo:

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

che è abbastanza semplice e diretto. E con Observableesso diventa rapidamente prolisso e piuttosto complicato.

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

E non è quello. Devi ancora fare qualcosa del genere:

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

Mi sto perdendo qualcosa qui? O è un caso sbagliato usare Observables? Quando si dovrebbe / si dovrebbe preferire un Observablesemplice callback?

Aggiornare

L'uso del retrofit è molto più semplice dell'esempio sopra, come mostrato da @Niels nella sua risposta o nel progetto di esempio U2020 di Jake Wharton . Ma essenzialmente la domanda rimane la stessa: quando si dovrebbe usare l'uno o l'altro modo?


puoi aggiornare il tuo link al file di cui stai parlando in U2020
letroll

Funziona ancora ...
Martynas Jurkus,

4
Amico, ho avuto gli stessi pensieri esattamente quando stavo leggendo RxJava, era la novità. Ho letto un esempio di retrofit (perché ne ho molta familiarità) di una semplice richiesta ed erano dieci o quindici righe di codice e la mia prima reazione è stata che mi prendevi in ​​giro = /. Inoltre non riesco a capire come questo sostituisca un bus di eventi, poiché il bus di evento ti disaccoppia dall'osservabile e rxjava reintroduce l'accoppiamento, a meno che non mi sbagli.
Lo-Tan,

Risposte:


348

Per semplici elementi di rete, i vantaggi di RxJava rispetto a Callback sono molto limitati. Il semplice esempio getUserPhoto:

RxJava:

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

Richiama:

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

La variante RxJava non è molto migliore della variante Callback. Per ora, ignoriamo la gestione degli errori. Facciamo un elenco di foto:

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

Richiama:

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

Ora, la variante RxJava non è ancora più piccola, anche se con Lambdas sarebbe più vicina alla variante Callback. Inoltre, se hai accesso al feed JSON, sarebbe un po 'strano recuperare tutte le foto quando visualizzi solo i PNG. Basta regolare il feed in modo da visualizzare solo i PNG.

Prima conclusione

Non rende più piccola la tua base di codice quando carichi un semplice JSON che hai preparato per essere nel formato giusto.

Ora, rendiamo le cose un po 'più interessanti. Diciamo che non vuoi solo recuperare l'utentePhoto, ma hai un clone di Instagram e vuoi recuperare 2 JSON: 1. getUserDetails () 2. getUserPhotos ()

Volete caricare questi due JSON in parallelo e quando entrambi vengono caricati, la pagina dovrebbe essere visualizzata. La variante di callback diventerà un po 'più difficile: devi creare 2 callback, archiviare i dati nell'attività e, se tutti i dati sono caricati, visualizzare la pagina:

Richiama:

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

Stiamo arrivando da qualche parte! Il codice di RxJava è ora grande quanto l'opzione di callback. Il codice RxJava è più robusto; Pensa a cosa accadrebbe se avessimo bisogno di un terzo JSON da caricare (come gli ultimi video)? RxJava avrebbe solo bisogno di una piccola modifica, mentre la variante Callback deve essere regolata in più posizioni (su ogni callback è necessario verificare se tutti i dati vengono recuperati).

Un altro esempio; vogliamo creare un campo di completamento automatico, che carica i dati usando Retrofit. Non vogliamo fare una webcall ogni volta che un EditText ha un TextChangedEvent. Quando si digita velocemente, solo l'ultimo elemento dovrebbe attivare la chiamata. Su RxJava possiamo usare l'operatore debounce:

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

Non creerò la variante di Callback ma capirai che questo è molto più lavoro.

Conclusione: RxJava è eccezionalmente buono quando i dati vengono inviati come flusso. Retrofit Observable spinge tutti gli elementi nello stream contemporaneamente. Questo non è particolarmente utile in sé rispetto a Callback. Ma quando ci sono più elementi spinti sullo stream e tempi diversi, e devi fare cose relative al timing, RxJava rende il codice molto più gestibile.


6
Che classe hai usato per il inputObservable? Ho molti problemi con il rimbalzo e vorrei saperne di più su questa soluzione.
Migore,

Il progetto @Migore RxBinding ha il modulo "Platform binding" che fornisce le classi come RxView, RxTextViewecc. Che possono essere utilizzate per il inputObservable.
bufera di neve

@Niels puoi spiegare come aggiungere la gestione degli errori? puoi creare un abbonato con onCompleted, onError e onNext se flatMap, Filter e quindi iscriviti? Grazie mille per la tua grande spiegazione.
Nicolas Jafelle,

4
Questo è un esempio formidabile!
Ye Lin Aung,

3
La migliore spiegazione di sempre!
Denis Nek,

66

Le cose osservabili sono già state fatte in Retrofit, quindi il codice potrebbe essere questo:

api.getUserPhoto(photoId)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<Photo>() {
         @Override
            public void call(Photo photo) {
                //save photo?
            }
     });

5
Sì, ma la domanda è quando preferire l'uno all'altro?
Christopher Perry,

7
Quindi, come è meglio del semplice callback?
Martynas Jurkus,

14
Gli osservabili di @MartynasJurkus possono essere utili se si desidera concatenare diverse funzioni. Ad esempio, il chiamante desidera ottenere una foto ritagliata in dimensioni 100x100. L'API può restituire una foto di qualsiasi dimensione, quindi è possibile mappare getUserPhoto osservabile su un altro ResizedPhotoObservable: il chiamante viene avvisato solo al termine del ridimensionamento. Se non è necessario utilizzarlo, non forzarlo.
ataulm

2
Un feedback minore. Non è necessario chiamare .subscribeOn (Schedulers.io ()) poiché RetroFit si occupa già di questo - github.com/square/retrofit/issues/430 (vedi la risposta di Jake)
hiBrianLee

4
@MartynasJurkus in aggiunta alle cose dette da ataulm, a differenza Callbackdi quanto tu possa annullare l'iscrizione Observeable, evitando così problemi di ciclo di vita comuni.
Dmitry Zaytsev il

35

Nel caso di getUserPhoto () i vantaggi per RxJava non sono grandi. Ma facciamo un altro esempio quando otterrai tutte le foto per un utente, ma solo quando l'immagine è PNG e non hai accesso a JSON per eseguire il filtro sul lato server.

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

Ora JSON restituisce un elenco di foto. Li appiattiremo sui singoli articoli. In questo modo, saremo in grado di utilizzare il metodo di filtro per ignorare le foto che non sono PNG. Successivamente, ci iscriveremo e riceveremo una richiamata per ogni singola foto, un ErrorHandler e una richiamata quando tutte le righe saranno state completate.

Punto TLDR essendo qui; la richiamata restituisce solo una richiamata per esiti positivi o negativi; RxJava Observable ti consente di mappare, ridurre, filtrare e molte altre cose.


Prima di tutto iscrivitiOn e observOn non sono necessari con il retrofit. Farà la chiamata di rete in modo asincrono e notificherà sullo stesso thread del chiamante. Quando aggiungi anche RetroLambda per ottenere espressioni lambda, diventa molto più bello.

ma posso farlo (l'immagine del filtro è PNG) in .subscribe () Perché dovrei usare il filtro? e non è necessario in flatmap
SERG

3
3 risposte da @Niels. Il primo risponde alla domanda. Nessun vantaggio per il 2 °
Raymond Chenon

stessa risposta come prima
Mayank Sharma

27

Con rxjava puoi fare più cose con meno codice.

Supponiamo che tu voglia implementare la ricerca istantanea nella tua app. Con i callback ti sei preoccupato di annullare l'iscrizione alla richiesta precedente e di iscriverti a quella nuova, gestire l'orientamento cambiando te stesso ... Penso che sia un sacco di codice e troppo prolisso.

Con rxjava è molto semplice.

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

se vuoi implementare la ricerca istantanea devi solo ascoltare TextChangeListener e chiamare photoModel.setUserId(EditText.getText());

Nel metodo onCreate di Frammento o attività ti iscrivi all'Osservabile che restituisce photoModel.subscribeToPhoto (), restituisce un Osservabile che emette sempre gli elementi emessi dall'ultimo Osservabile (richiesta).

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

Inoltre, se PhotoModel è un Singleton, ad esempio, non devi preoccuparti dei cambiamenti di orientamento, perché BehaviorSubject emette l'ultima risposta del server, indipendentemente da quando ti iscrivi.

Con queste righe di codice abbiamo implementato una ricerca istantanea e gestiamo i cambiamenti di orientamento. Pensi di poterlo implementare con callback con meno codice? Ne dubito.


Non pensi che rendere Singleton una classe PhotoModel sia di per sé una restrizione? Immagina che non sia un profilo utente ma una serie di foto per più utenti, non otterremo solo l'ultima foto utente richiesta? Inoltre, richiedere un dato su ogni cambio di orientamento mi sembra un po 'off, cosa ne pensi?
Farid,

2

Di solito andiamo con la seguente logica:

  1. Se si tratta di una semplice chiamata a risposta singola, è meglio Callback o Future.
  2. Se si tratta di una chiamata con risposte multiple (stream) o in caso di interazione complessa tra chiamate diverse (vedere la risposta di @Niels ), gli osservabili sono migliori.

1

Dai campioni e dalle conclusioni in altre risposte, penso che non vi sia alcuna grande differenza per i compiti semplici in una o due fasi. Tuttavia, Callback è semplice e diretto. RxJava è più complicato e troppo grande per un compito semplice. C'è la terza soluzione di: AbacusUtil . Vorrei implementare i casi d'uso sopra descritti con tutte e tre le soluzioni: Callback, RxJava, CompletableFuture (AbacusUtil) con Retrolambda :

Recupera foto dalla rete e salva / visualizza sul dispositivo:

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

Carica i dettagli dell'utente e la foto in parallelo

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

4
OP non chiedeva suggerimenti per una nuova biblioteca
forresthopkinsa,

1

Personalmente preferisco usare Rx per ottenere risposte api nei casi in cui devo fare filtri, mappe o qualcosa del genere sui dati O nei casi in cui devo fare altre chiamate api basate sulle risposte delle chiamate precedenti


0

Sembra che tu stia reinventando la ruota, quello che stai facendo è già implementato nel retrofit.

Ad esempio, puoi guardare RestAdapterTest.java di retrofit , in cui definiscono un'interfaccia con Observable come tipo restituito, e quindi utilizzarla .


Grazie per la risposta. Vedo cosa stai dicendo, ma non sono ancora sicuro di come implementarlo. Potresti fornire un semplice esempio usando l'implementazione di retrofit esistente in modo che io possa assegnarti la taglia?
Yehosef

@Yehosef qualcuno ha eliminato il suo commento o fingevi di essere OP? ))
Farid

Puoi assegnare una taglia sulla domanda di qualcun altro. Quando l'ho chiesto, avevo davvero bisogno della risposta alla domanda, quindi ho offerto una generosità. Se passi con il mouse sul premio Bounty, vedrai che è stato assegnato da me.
Yehosef,

0

Quando crei un'app per divertimento, un progetto per animali domestici o un POC o il primo prototipo usi semplici classi Android / Java come core callback, attività asincrone, looper, thread, ecc. Sono semplici da usare e non richiedono alcun Integrazione lib di terze parti. Una grande integrazione della biblioteca solo per costruire un progetto immutabile in tempi brevi è illogica quando qualcosa di simile può essere fatto subito.

Tuttavia, questi sono come un coltello molto affilato. L'uso di questi in un ambiente di produzione è sempre interessante, ma avranno anche delle conseguenze. È difficile scrivere codice concorrente sicuro se non si è esperti di codifica pulita e principi SOLIDI. Dovrai mantenere un'architettura adeguata per facilitare i cambiamenti futuri e migliorare la produttività del team.

D'altra parte, le librerie di concorrenza come RxJava, Co-routine ecc. Sono provate e testate oltre un miliardo di volte per aiutare a scrivere codice concorrente pronto per la produzione. Ora, di nuovo, non è che usando queste librerie non si stia scrivendo codice simultaneo o si sottragga via tutta la logica di concorrenza. Lo sei ancora. Ma ora è visibile e applica un modello chiaro per la scrittura di codice simultaneo in tutta la base di codice e, soprattutto, in tutto il team di sviluppo.

Questo è uno dei maggiori vantaggi dell'utilizzo di un framework di concorrenza anziché delle semplici vecchie classi core che si occupano della concorrenza grezza. Tuttavia, non fraintendermi. Sono un grande sostenitore della limitazione delle dipendenze della libreria esterna, ma in questo caso specifico, dovrai creare un framework personalizzato per la tua base di codice che è un compito che richiede tempo e può essere fatto solo dopo un'esperienza anticipata. Pertanto, i framework di concorrenza sono preferiti rispetto all'uso di classi semplici come callback, ecc.


TL'DR

Se stai già utilizzando RxJava per la codifica simultanea in tutta la base di codice, utilizza semplicemente RxJava Observable / Flowable. Una domanda saggia da porsi è se dovessi usare gli osservabili ai flowables. Altrimenti, vai avanti e usa un callable.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.