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