web-dev-qa-db-fra.com

Séquence combinée de RxJava des demandes

Le problème

J'ai deux Apis. Api 1 me fournit une liste d'éléments et Api 2 me donne des informations plus détaillées pour chacun des éléments que j'ai obtenus d'Api 1. La façon dont je l'ai résolue jusqu'à présent entraîne une mauvaise performance.

La question

Solution efficace et rapide à ce problème avec l'aide de Retrofit et de RxJava.

Mon approche

Au moment où ma solution ressemble à ceci:

Étape 1: La modernisation exécute Single<ArrayList<Information>> à partir de Api 1.

Étape 2: Je parcoure ces éléments et lance une demande pour chacun d’eux à Api 2.

Étape 3: Retours de conversion exécute séquentiellement Single<ExtendedInformation> pour Chaque article

Étape 4: Une fois tous les appels de Api 2 complètement exécutés, je crée un nouvel objet pour tous les éléments combinant les informations et les informations étendues.

Mon code

 public void addExtendedInformations(final Information[] informations) {
        final ArrayList<InformationDetail> informationDetailArrayList = new ArrayList<>();
        final JSONRequestRatingHelper.RatingRequestListener ratingRequestListener = new JSONRequestRatingHelper.RatingRequestListener() {
            @Override
            public void onDownloadFinished(Information baseInformation, ExtendedInformation extendedInformation) {
                informationDetailArrayList.add(new InformationDetail(baseInformation, extendedInformation));
                if (informationDetailArrayList.size() >= informations.length){
                    listener.onAllExtendedInformationLoadedAndCombined(informationDetailArrayList);
                }
            }
        };

        for (Information information : informations) {
            getExtendedInformation(ratingRequestListener, information);
        }
    }

    public void getRatingsByTitle(final JSONRequestRatingHelper.RatingRequestListener ratingRequestListener, final Information information) {
        Single<ExtendedInformation> repos = service.findForTitle(information.title);
        disposable.add(repos.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribeWith(new DisposableSingleObserver<ExtendedInformation>() {
            @Override
            public void onSuccess(ExtendedInformation extendedInformation) {
                    ratingRequestListener.onDownloadFinished(information, extendedInformation);
            }

            @Override
            public void onError(Throwable e) {
                ExtendedInformation extendedInformation = new ExtendedInformation();
                ratingRequestListener.onDownloadFinished(extendedInformation, information);
            }
        }));
    }

    public interface RatingRequestListener {

        void onDownloadFinished(Information information, ExtendedInformation extendedInformation);

    }
16
Mayr Technologies

tl; dr utilise concatMapEager ou flatMap et exécute des sous-appels de manière asynchrone ou sur un planificateur.


longue histoire

Je ne suis pas un développeur Android, ma question se limitera donc à RxJava (versions 1 et 2).

Si je comprends bien, le flux nécessaire est le suivant: 

some query param 
  \--> Execute query on API_1 -> list of items
          |-> Execute query for item 1 on API_2 -> extended info of item1
          |-> Execute query for item 2 on API_2 -> extended info of item1
          |-> Execute query for item 3 on API_2 -> extended info of item1
          ...
          \-> Execute query for item n on API_2 -> extended info of item1
  \----------------------------------------------------------------------/
      |
      \--> stream (or list) of extended item info for the query param

En supposant que la modernisation ait généré les clients pour 

interface Api1 {
    @GET("/api1") Observable<List<Item>> items(@Query("param") String param);
}

interface Api2 {
    @GET("/api2/{item_id}") Observable<ItemExtended> extendedInfo(@Path("item_id") String item_id);
}

Si l'ordre de l'élément n'est pas important, il est possible d'utiliser flatMap uniquement: 

api1.items(queryParam)
    .flatMap(itemList -> Observable.fromIterable(itemList)))
    .flatMap(item -> api2.extendedInfo(item.id()))
    .subscribe(...)

Mais uniquement si le constructeur de modernisation est configuré avec

  • Soit avec l'adaptateur asynchrone (les appels seront mis en file d'attente dans l'exécuteur interne okhttp). Personnellement, je pense que ce n'est pas une bonne idée, car vous n'avez aucun contrôle sur cet exécuteur.

    .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()
    
  • Ou avec l'adaptateur basé sur le planificateur (les appels seront planifiés sur le planificateur RxJava). Ce serait mon option préférée, car vous choisissez explicitement le planificateur à utiliser, ce sera probablement le planificateur IO, mais vous êtes libre d’en essayer un autre.

    .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
    

La raison en est que flatMap souscrira à chaque observable créé par api2.extendedInfo(...) et les fusionnera dans l'observable obtenu. Les résultats apparaîtront donc dans l'ordre dans lequel ils ont été reçus.

Si le client de modernisation n'est pas défini pour être asynchrone ou configuré pour s'exécuter sur un planificateur, il est possible d'en définir un: 

api1.items(queryParam)
    .flatMap(itemList -> Observable.fromIterable(itemList)))
    .flatMap(item -> api2.extendedInfo(item.id()).subscribeOn(Schedulers.io()))
    .subscribe(...)

Cette structure est presque identique à la précédente. Elle indique localement sur quel programmateur chaque api2.extendedInfo est censé être exécuté.

Il est possible d’affiner le paramètre maxConcurrency de flatMap pour contrôler le nombre de demandes que vous souhaitez effectuer simultanément. Bien que je sois prudent sur ce point, vous ne voulez pas exécuter toutes les requêtes en même temps. La valeur par défaut maxConcurrency est suffisante (128).

Maintenant, si l’ordre de la requête initiale est important . concatMap est généralement l'opérateur qui fait la même chose que flatMap dans l'ordre mais de manière séquentielle, ce qui s'avère être lent si le code doit attendre que toutes les sous-requêtes soient exécutées. La solution est cependant un peu plus loin avec concatMapEager, celle-ci souscrira à observable dans l'ordre et tamponnera les résultats si nécessaire.

En supposant que les clients de mise à niveau sont asynchrones ou exécutés sur un planificateur spécifique:

api1.items(queryParam)
    .flatMap(itemList -> Observable.fromIterable(itemList)))
    .concatMapEager(item -> api2.extendedInfo(item.id()))
    .subscribe(...)

Ou si le planificateur doit être défini localement:

api1.items(queryParam)
    .flatMap(itemList -> Observable.fromIterable(itemList)))
    .concatMapEager(item -> api2.extendedInfo(item.id()).subscribeOn(Schedulers.io()))
    .subscribe(...)

Il est également possible de régler la concurrence d'accès dans cet opérateur.


De plus, si l'API renvoie Flowable, il est possible d'utiliser .parallel qui est toujours en version bêta à ce moment-là dans RxJava 2.1.7. Mais alors les résultats ne sont pas en ordre et je ne sais pas (encore?) Comment les commander sans les trier.

api.items(queryParam) // Flowable<Item>
   .parallel(10)
   .runOn(Schedulers.io())
   .map(item -> api2.extendedInfo(item.id()))
   .sequential();     // Flowable<ItemExtended>
17
Brice

l'opérateur flatMap est conçu pour répondre à ces types de flux de travail.

je vais décrire les grandes lignes avec un exemple simple en cinq étapes. Espérons que vous pourrez facilement reconstruire les mêmes principes dans votre code:

@Test fun flatMapExample() {
    // (1) constructing a fake stream that emits a list of values
    Observable.just(listOf(1, 2, 3, 4, 5))
            // (2) convert our List emission into a stream of its constituent values 
            .flatMap { numbers -> Observable.fromIterable(numbers) }
            // (3) subsequently convert each individual value emission into an Observable of some 
            //     newly calculated type
            .flatMap { number ->
                when(number) {
                       1 -> Observable.just("A1")
                       2 -> Observable.just("B2")
                       3 -> Observable.just("C3")
                       4 -> Observable.just("D4")
                       5 -> Observable.just("E5")
                    else -> throw RuntimeException("Unexpected value for number [$number]")
                }
            }
            // (4) collect all the final emissions into a list
            .toList()
            .subscribeBy(
                    onSuccess = {
                        // (5) handle all the combined results (in list form) here
                        println("## onNext($it)")
                    },
                    onError = { error ->
                        println("## onError(${error.message})")
                    }
            )
}

(incidemment, si l'ordre des émissions est important, envisagez plutôt d'utiliser concatMap).

j'espère que ça aide.

2
homerman

Vérifiez ci-dessous cela fonctionne.

Supposons que vous ayez plusieurs appels sur le réseau dont vous avez besoin pour vous préparer afin d'obtenir des informations sur les utilisateurs de Github et les événements des utilisateurs de Github, par exemple.

Et vous voulez attendre que chacun revienne avant de mettre à jour l'interface utilisateur. RxJava peut vous aider ici . Définissons d’abord notre objet Retrofit pour accéder à l’API de Github, puis définissons deux observables pour l’appel aux deux requêtes réseau.

Retrofit repo = new Retrofit.Builder()
        .baseUrl("https://api.github.com")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        .build();

Observable<JsonObject> userObservable = repo
        .create(GitHubUser.class)
        .getUser(loginName)
        .subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread());

Observable<JsonArray> eventsObservable = repo
        .create(GitHubEvents.class)
        .listEvents(loginName)
        .subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread());

Interface utilisée pour cela comme ci-dessous:

public interface GitHubUser {
  @GET("users/{user}")
  Observable<JsonObject> getUser(@Path("user") String user);
}

public interface GitHubEvents {
  @GET("users/{user}/events")
  Observable<JsonArray> listEvents(@Path("user") String user);
}

Après avoir utilisé la méthode Zip de RxJava, combinez nos deux observables et attendez qu’ils s’achèvent avant de créer un nouvel observable.

Observable<UserAndEvents> combined = Observable.Zip(userObservable, eventsObservable, new Func2<JsonObject, JsonArray, UserAndEvents>() {
  @Override
  public UserAndEvents call(JsonObject jsonObject, JsonArray jsonElements) {
    return new UserAndEvents(jsonObject, jsonElements);
  }
});

Enfin, appelons la méthode subscribe sur notre nouvel Observable combiné:

combined.subscribe(new Subscriber<UserAndEvents>() {
          ...
          @Override
          public void onNext(UserAndEvents o) {
            // You can access the results of the 
            // two observabes via the POJO now
          }
        });

Plus besoin d'attendre dans les discussions, etc., la fin des appels réseau. RxJava a fait tout cela pour vous dans Zip () . Espérons que ma réponse vous aidera.

1
jyubin patel

J'ai résolu un problème similaire avec RxJava2. L'exécution parallèle de demandes d'API 2 accélère légèrement le travail.

private InformationRepository informationRepository;

//init....

public Single<List<FullInformation>> getFullInformation() {
    return informationRepository.getInformationList()
            .subscribeOn(Schedulers.io())//I usually write subscribeOn() in the repository, here - for clarity
            .flatMapObservable(Observable::fromIterable)
            .flatMapSingle(this::getFullInformation)
            .collect(ArrayList::new, List::add);

}

private Single<FullInformation> getFullInformation(Information information) {
    return informationRepository.getExtendedInformation(information)
            .map(extendedInformation -> new FullInformation(information, extendedInformation))
            .subscribeOn(Schedulers.io());//execute requests in parallel
}

InformationRepository - juste une interface. Sa mise en œuvre n'est pas intéressante pour nous.

public interface InformationRepository {

    Single<List<Information>> getInformationList();

    Single<ExtendedInformation> getExtendedInformation(Information information);
}

FullInformation - conteneur pour le résultat. 

public class FullInformation {

    private Information information;
    private ExtendedInformation extendedInformation;

    public FullInformation(Information information, ExtendedInformation extendedInformation) {
        this.information = information;
        this.extendedInformation = extendedInformation;
    }
}
0
Anrimian

Essayez d'utiliser l'opérateur Observable.Zip(). Il attendra que les deux appels Api soient terminés avant de poursuivre le flux. Ensuite, vous pouvez insérer une logique en appelant flatMap() par la suite.

http://reactivex.io/documentation/operators/Zip.html

0
Fraudlic