34

The Problem

I have two Apis. Api 1 gives me a List of Items and Api 2 gives me more detailed Information for each of the items I got from Api 1. The way I solved it so far results in bad Performance.

The Question

Efficent and fast solution to this Problem with the help of Retrofit and RxJava.

My Approach

At the Moment my Solution Looks like this:

Step 1: Retrofit executes Single<ArrayList<Information>> from Api 1.

Step 2: I iterate through this Items and make a request for each to Api 2.

Step 3: Retrofit Returns Sequentially executes Single<ExtendedInformation> for each item

Step 4: After all calls form Api 2 completely executed I create a new Object for all Items combining the Information and Extended Information.

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

    }
Mayr Technologies
  • 725
  • 3
  • 10
  • 19
  • 1
    Why does your methods need to be `synchronized` ? – bric3 Dec 06 '17 at 10:15
  • @Brice There was a time they Needed to be synchronized and I forgot to remove it. Thanks :) – Mayr Technologies Dec 06 '17 at 12:09
  • You sort of put yourself in this state. Questions: 1. Does it really need to query row-by-row? (bulk queries usually are much more efficient). 2. Do you really have to wait for all results to arrive before you can procceed? Can you opt to show results immediately as they arrive in blocks? – M. Prokhorov Dec 19 '17 at 13:20

5 Answers5

43

tl;dr use concatMapEager or flatMap and execute sub-calls asynchronously or on a schedulers.


long story

I'm not an android developer, so my question will be limited to pure RxJava (version 1 and version 2).

If I get the picture right the needed flow is :

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

Assuming Retrofit generated the clients for

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

If the order of the item is not important, then it is possible to use flatMap only:

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

But only if the retrofit builder is configured with

  • Either with the async adapter (calls will be queued in the okhttp internal executor). I personally think this is not a good idea, because you don't have control over this executor.

    .addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync()
    
  • Or with the scheduler based adapter (calls will be scheduled on the RxJava scheduler). It would my preferred option, because you explicitly choose which scheduler is used, it will be most likely the IO scheduler, but you are free to try a different one.

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

The reason is that flatMap will subscribe to each observable created by api2.extendedInfo(...) and merge them in the resulting observable. So results will appear in the order they are received.

If the retrofit client is not set to be async or set to run on a scheduler, it is possible to set one :

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

This structure is almost identical to the previous one execpts it indicates locally on which scheduler each api2.extendedInfo is supposed to run.

It is possible to tune the maxConcurrency parameter of flatMap to control how many request you want to perform at the same time. Although I'd be cautious on this one, you don't want run all queries at the same time. Usually the default maxConcurrency is good enough (128).

Now if order of the original query matter. concatMap is usually the operator that does the same thing as flatMap in order but sequentially, which turns out to be slow if the code need to wait for all sub-queries to be performed. The solution though is one step further with concatMapEager, this one will subscribe to observable in order, and buffer the results as needed.

Assuming retrofit clients are async or ran on a specific scheduler :

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

Or if the scheduler has to be set locally :

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

It is also possible to tune the concurrency in this operator.


Additionally if the Api is returning Flowable, it is possible to use .parallel that is still in beta at this time in RxJava 2.1.7. But then results are not in order and I don't know a way (yet?) to order them without sorting after.

api.items(queryParam) // Flowable<Item>
   .parallel(10)
   .runOn(Schedulers.io())
   .map(item -> api2.extendedInfo(item.id()))
   .sequential();     // Flowable<ItemExtended>
bric3
  • 40,072
  • 9
  • 91
  • 111
  • 1
    `item.id()` is available from `item` which is any item found in the `itemList` (this list is _flat mapped_ by `.flatMap(itemList -> Observable.fromIterable(itemList))` in order to _pipe_ each element of the list as a single element in the observable flux). – bric3 Dec 13 '17 at 12:32
  • Thanks for your answer! One last thing I don't understand. How can I get a list of the combined Responses? – Mayr Technologies Dec 13 '17 at 12:46
  • And it Shows me an error if I don't use .flatMapIterable(itemList -> Arrays.asList(itemList.items)) before I call .flatMap(item -> api2.extendedInfo(item.id()) – Mayr Technologies Dec 13 '17 at 12:52
  • 1
    If you need to _accumulate_ the `extendedInfo` into a list, it is possible to use the `.toList()` operator. If using RxJava 2 `toList` returns `Single>` to get an `Observable>` it is possible to chain with `.toList().toObservable()` – bric3 Dec 13 '17 at 12:55
  • 1
    Yes it is necessary to _flat map_ the list returned by the api1 call. Either by `.flatMap(itemList -> Observable.fromIterable(itemList))` or by `.flatMapIterable(itemList -> Arrays.asList(itemList.items))` (which can be simplified to `.flatMapIterable(itemList -> itemList)` if `itemList` implements `Iterable` which quite likely). – bric3 Dec 13 '17 at 13:02
  • Thanks a lot, for your detailed answer! – Mayr Technologies Dec 19 '17 at 23:14
  • 1
    This is a stellar answer and I just wanted to thank you for taking the time to explain all of it in such detail. The Call Adapter Factory on Schedulers explanation is extremely helpful. – Matthew Bahr May 17 '18 at 22:03
  • Hi, what about synch requests? I tried this answer, but requests sent like asynch. I need synch execution. Do you know how to make it? Thx. – Georgiy Chebotarev Dec 03 '19 at 16:53
  • @GeorgiyChebotarev Can you be more explicit, currently I can only suggest to build retrofit with this adapter instead `.addCallAdapterFactory(RxJava2CallAdapterFactory.create()` – bric3 Dec 05 '19 at 13:34
  • @Brice bro, thanks a lot for your answer. But I have a question. How to combine api1 and api2 info and show the combined info in RecyclerView? – Aminul Haque Aome Jul 20 '21 at 05:42
  • @AminulHaqueAome Sorry I can't answer this part on the `RecyclerView` as I'm not an Android developer. – bric3 Jul 20 '21 at 11:16
  • @Brice bro, as you said, you are not an android dev, so I can't force you to solve. But can you have a look at this question https://stackoverflow.com/questions/68439959/in-android-make-a-list-of-api-call-inside-one-api-call – Aminul Haque Aome Jul 20 '21 at 19:41
7

the flatMap operator is designed to cater to these types of workflows.

i'll outline the broad strokes with a simple five step example. hopefully you can easily reconstruct the same principles in your 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})")
                    }
            )
}

(incidentally, if the order of the emissions matter, look at using concatMap instead).

i hope that helps.

homerman
  • 3,369
  • 1
  • 16
  • 32
  • thanks for your answer. However I am not sure how to apply that to my example. – Mayr Technologies Dec 06 '17 at 12:08
  • This does not answer the question exactly, e.g. the list is not frozen it is the result of query, so the kotlin `when` statement cannot be used, not even accounting projects that don't use kotlin, plus it doesn't say why this or how it could be faster. Plus `flatMap` by default is not concurrent so it doesn't really speed up things. – bric3 Dec 06 '17 at 13:07
  • @Brice the sample code implements meaningless, yet, simple logic in order to demonstrate the broader point. `when` isn't really salient to what i was trying to demonstrate. you're correct that it doesn't demonstrate any particular improvement in speed, but more importantly (in my opinion) it does demonstrate idiomatic Rx in transforming the stream rather than breaking it and reverting to some form of "callback hell" as the original solution seemed to do. your (well-written) solution even acknowledges that there are negligible performance gains to be had: "Usually the default is good enough". – homerman Dec 06 '17 at 14:29
  • The callback things is indeed to be addressed, but that's not part of the question. This part didn't imply that performance was negligible. The real trick is to perform sub queries asynchronously in combination of `flatMap` or `concatMapEager`. The quoted part refer to the `maxConcurrency` parameter which allows to tune how many sub-Observables can be subscribed at a time by `flatMap` or `concatMapEager`. – bric3 Dec 06 '17 at 14:37
  • @Brice i don't agree that idiomatic usage of the library isn't part of the solution when the poster asked for an "efficent and fast" solution. to be fair i think you and i have addressed these two points respectively. (but i do like that you've elaborated on the permutations of `concatMap`). – homerman Dec 06 '17 at 18:01
  • Fair point about the idiomatics. But I'm a picky guy, sorry about that ; `when` is a kotlin construct, as far as I know it doesn't perform things asynchronously so this won't make the code faster, however it makes it more RxJava style, which is good indeed. Also the code throws an exception `flatMap`, while it should instead return a `Observable.error`. – bric3 Dec 08 '17 at 11:47
2

Check below it's working.

Say you have multiple network calls you need to make–cals to get Github user information and Github user events for example.

And you want to wait for each to return before updating the UI. RxJava can help you here. Let’s first define our Retrofit object to access Github’s API, then setup two observables for the two network requests call.

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

Used Interface for it like below:

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

After we use RxJava’s zip method to combine our two Observables and wait for them to complete before creating a new 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);
  }
});

Finally let’s call the subscribe method on our new combined Observable:

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

No more waiting in threads etc for network calls to finish. RxJava has done all that for you in zip(). hope my answer helps you.

Jyubin Patel
  • 1,373
  • 7
  • 17
1

I solved a similar problem with RxJava2. Execution of requests for Api 2 in parallel slightly speeds up the work.

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 - just interface. Its implementation is not interesting for us.

public interface InformationRepository {

    Single<List<Information>> getInformationList();

    Single<ExtendedInformation> getExtendedInformation(Information information);
}

FullInformation - container for result.

public class FullInformation {

    private Information information;
    private ExtendedInformation extendedInformation;

    public FullInformation(Information information, ExtendedInformation extendedInformation) {
        this.information = information;
        this.extendedInformation = extendedInformation;
    }
}
Anrimian
  • 4,257
  • 4
  • 22
  • 30
0

Try using Observable.zip() operator. It will wait until both Api calls are finished before continuing the stream. Then you can insert some logic by calling flatMap() afterwards.

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

Fraudlic
  • 66
  • 5