RxJava Объединить Последовательность Запросов

Проблема

у меня два API. Api 1 дает мне список элементов, а Api 2 дает мне более подробную информацию для каждого из элементов, которые я получил от Api 1. То, как я решил это до сих пор, приводит к плохой производительности.

Вопрос

эффективное и быстрое решение этой проблемы с помощью Retrofit и RxJava.

Мой Подход

на данный момент мое решение выглядит так:

Шаг 1: Retrofit выполняет Single<ArrayList<Information>> из Api 1.

Шаг 2: я повторяю эти элементы и делаю запрос для каждого Api 2.

Шаг 3: Retrofit возвращает последовательно выполняется Single<ExtendedInformation> для каждый пункт

Шаг 4: После того, как все вызовы формы Api 2 полностью выполнены, я создаю новый объект для всех элементов, объединяющих информацию и расширенную информацию.

Код

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

    }

5 ответов


tl; dr использовать concatMapEager или flatMap и выполнять подзвонки асинхронно или на планировщиках.


долгая история

я не разработчик android, поэтому мой вопрос будет ограничен чистой RxJava (Версия 1 и версия 2).

если я правильно понял, необходимый поток:

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

предполагая, что Retrofit сгенерировал клиентов для

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

если заказ деталя не важно, тогда можно использовать flatMap только:

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

но только если конструктор дооснащения настроен на

  • либо с асинхронным адаптером (вызовы будут поставлены в очередь во внутреннем исполнителе okhttp). Я лично думаю, что это не очень хорошая идея, потому что у вас нет контроля над этим исполнителем.

    .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()
    
  • или с адаптером на основе планировщика (вызовы будут запланированы на RxJava scheduler). Это был бы мой предпочтительный вариант, потому что вы явно выбираете, какой планировщик используется, это, скорее всего, планировщик ввода-вывода, но вы можете попробовать другой.

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

причина в том, что flatMap будет подписываться на каждый наблюдаемый, созданный api2.extendedInfo(...) и объединить их в результате наблюдаемых. Поэтому результаты будут появляться в том порядке, в котором они получены.

если установки клиента это не установить асинхронность или установить для запуска в планировщике, можно установить один:

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

эта структура почти идентична предыдущей execpts он указывает локально на каком планировщике каждый api2.extendedInfo должно работать.

можно настроить на flatMap чтобы контролировать, сколько запросов вы хотите выполнить одновременно. Хотя я был бы осторожен в этом, вы не хотите запускать все запросы одновременно время. Обычно по умолчанию maxConcurrency достаточно хорош (128).

теперь, если порядок исходного запроса имеет значение. concatMap обычно это оператор, который делает то же самое, что и flatMap по порядку, но последовательно, что оказывается медленным, если код должен ждать выполнения всех подзапросов. Решение, хотя на один шаг дальше с concatMapEager, этот будет подписываться на observable в порядке и буферизировать результаты по мере необходимости.

предполагая клиенты дооснащения асинхронны или выполняются на определенном планировщике:

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

или если планировщик должен быть установлен локально :

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

также можно настроить параллелизм в этом операторе.


дополнительно, если Api возвращается Flowable можно использовать .parallel это все еще в бета-версии в это время в RxJava 2.1.7. Но тогда результаты не в порядке, и я не знаю как (пока?) заказать их без сортировки после.

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

на flatMap оператор предназначен для обслуживания этих типов рабочих процессов.

я обрисую широкие штрихи с простым примером из пяти шагов. надеюсь, вы можете легко восстановить те же принципы в своем коде:

@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})")
                    }
            )
}

(кстати, если порядок выбросов имеет значение, посмотрите на использование concatMap вместо).

надеюсь, это поможет.


Проверьте ниже, что он работает.

скажем, у вас есть несколько сетевых вызовов, которые вам нужно сделать, чтобы получить информацию о пользователе Github и события пользователя Github, например.

и вы хотите ждать каждого, чтобы вернуться перед обновлением пользовательского интерфейса. RxJava может помочь вам здесь. Давайте сначала определим наш модифицированный объект для доступа к API Github, а затем настроим два наблюдаемых для вызова двух сетевых запросов.

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

используется интерфейс для него, как ниже:

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

после того, как мы используем RxJava zip метод объединения наших двух наблюдаемых и ждать их завершения, прежде чем создавать новый наблюдаемый.

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

наконец, давайте вызовем метод subscribe на нашем новом комбинированном наблюдаемом:

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

нет больше ожидания в потоках и т. д. Для завершения сетевых вызовов. RxJava сделал все это для вас в zip(). надеюсь, мой ответ поможет вам.


я решил аналогичную проблему с RxJava2. Выполнение запросов на Api 2 параллельно несколько ускоряет работу.

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 - просто интерфейс. Его реализация нам не интересна.

public interface InformationRepository {

    Single<List<Information>> getInformationList();

    Single<ExtendedInformation> getExtendedInformation(Information information);
}

FullInformation-контейнер для результата.

public class FullInformation {

    private Information information;
    private ExtendedInformation extendedInformation;

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

попробуйте использовать Observable.zip() оператора. Он будет ждать, пока оба вызова Api не будут завершены, прежде чем продолжить поток. Затем вы можете вставить некоторую логику, вызвав flatMap() далее.

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