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()
далее.