Combine a list of Observables and wait until all completed
Asked Answered
R

8

103

TL;DR How to convert Task.whenAll(List<Task>) into RxJava?

My existing code uses Bolts to build up a list of asynchronous tasks and waits until all of those tasks finish before performing other steps. Essentially, it builds up a List<Task> and returns a single Task which is marked as completed when all tasks in the list complete, as per the example on the Bolts site.

I'm looking to replace Bolts with RxJava and I'm assuming this method of building up a list of async tasks (size not known in advance) and wrapping them all into a single Observable is possible, but I don't know how.

I've tried looking at merge, zip, concat etc... but can't get to work on the List<Observable> that I'd be building up as they all seem geared to working on just two Observables at a time if I understand the docs correctly.

I'm trying to learn RxJava and am still very new to it so forgive me if this is an obvious question or explained in the docs somewhere; I have tried searching. Any help would be much appreciated.

Remanence answered 12/2, 2016 at 8:28 Comment(0)
B
78

It sounds like you're looking for the Zip operator.

There are a few different ways of using it, so let's look at an example. Say we have a few simple observables of different types:

Observable<Integer> obs1 = Observable.just(1);
Observable<String> obs2 = Observable.just("Blah");
Observable<Boolean> obs3 = Observable.just(true);

The simplest way to wait for them all is something like this:

Observable.zip(obs1, obs2, obs3, (Integer i, String s, Boolean b) -> i + " " + s + " " + b)
.subscribe(str -> System.out.println(str));

Note that in the zip function, the parameters have concrete types that correspond to the types of the observables being zipped.

Zipping a list of observables is also possible, either directly:

List<Observable<?>> obsList = Arrays.asList(obs1, obs2, obs3);

Observable.zip(obsList, (i) -> i[0] + " " + i[1] + " " + i[2])
.subscribe(str -> System.out.println(str));

...or by wrapping the list into an Observable<Observable<?>>:

Observable<Observable<?>> obsObs = Observable.from(obsList);

Observable.zip(obsObs, (i) -> i[0] + " " + i[1] + " " + i[2])
.subscribe(str -> System.out.println(str));

However, in both of these cases, the zip function can only accept a single Object[] parameter since the types of the observables in the list are not known in advance as well as their number. This means that that the zip function would have to check the number of parameters and cast them accordingly.

Regardless, all of the above examples will eventually print 1 Blah true

EDIT: When using Zip, make sure that the Observables being zipped all emit the same number of items. In the above examples all three observables emitted a single item. If we were to change them to something like this:

Observable<Integer> obs1 = Observable.from(new Integer[]{1,2,3}); //Emits three items
Observable<String> obs2 = Observable.from(new String[]{"Blah","Hello"}); //Emits two items
Observable<Boolean> obs3 = Observable.from(new Boolean[]{true,true}); //Emits two items

Then 1, Blah, True and 2, Hello, True would be the only items passed into the zip function(s). The item 3would never be zipped since the other observables have completed.

Bifid answered 12/2, 2016 at 12:48 Comment(4)
This wont work if one of the calls fails. In that case all calls will be lost.Grandiloquence
@Grandiloquence you can skip error by use onErrorResumeNext, example: Observable.zip(ob1, ob2........).onErrorResumeNext(Observable.<String>empty())Perfecto
What if I have a 100 observables ?Sandry
To handle error what's the best approach hereTippett
N
89

You can use flatMap in case you have dynamic tasks composition. Something like this:

public Observable<Boolean> whenAll(List<Observable<Boolean>> tasks) {
    return Observable.from(tasks)
            //execute in parallel
            .flatMap(task -> task.observeOn(Schedulers.computation()))
            //wait, until all task are executed
            //be aware, all your observable should emit onComplete event
            //otherwise you will wait forever
            .toList()
            //could implement more intelligent logic. eg. check that everything is successful
            .map(results -> true);
}

Another good example of parallel execution

Note: I do not really know your requirements for error handling. For example, what to do if only one task fails. I think you should verify this scenario.

Nader answered 12/2, 2016 at 14:44 Comment(4)
This should be the accepted answer considering that question states "when all tasks in the list complete". zip notifies about completion as soon as one of the tasks completed and thus is not applicable.Voracity
@Nader : Can you Update the Answer with Java7 Syntax (Not the lambda) version?Pennsylvanian
@PoojaGaikwad With lambda it's more readable. Just replace first lambda with new Func1<Observable<Boolean>, Observable<Boolean>>()... and second one with new Func1<List<Boolean>, Boolean>()Nader
@soshial RxJava 2 is the worst thing that ever happened with RxJava, yeahCoel
B
78

It sounds like you're looking for the Zip operator.

There are a few different ways of using it, so let's look at an example. Say we have a few simple observables of different types:

Observable<Integer> obs1 = Observable.just(1);
Observable<String> obs2 = Observable.just("Blah");
Observable<Boolean> obs3 = Observable.just(true);

The simplest way to wait for them all is something like this:

Observable.zip(obs1, obs2, obs3, (Integer i, String s, Boolean b) -> i + " " + s + " " + b)
.subscribe(str -> System.out.println(str));

Note that in the zip function, the parameters have concrete types that correspond to the types of the observables being zipped.

Zipping a list of observables is also possible, either directly:

List<Observable<?>> obsList = Arrays.asList(obs1, obs2, obs3);

Observable.zip(obsList, (i) -> i[0] + " " + i[1] + " " + i[2])
.subscribe(str -> System.out.println(str));

...or by wrapping the list into an Observable<Observable<?>>:

Observable<Observable<?>> obsObs = Observable.from(obsList);

Observable.zip(obsObs, (i) -> i[0] + " " + i[1] + " " + i[2])
.subscribe(str -> System.out.println(str));

However, in both of these cases, the zip function can only accept a single Object[] parameter since the types of the observables in the list are not known in advance as well as their number. This means that that the zip function would have to check the number of parameters and cast them accordingly.

Regardless, all of the above examples will eventually print 1 Blah true

EDIT: When using Zip, make sure that the Observables being zipped all emit the same number of items. In the above examples all three observables emitted a single item. If we were to change them to something like this:

Observable<Integer> obs1 = Observable.from(new Integer[]{1,2,3}); //Emits three items
Observable<String> obs2 = Observable.from(new String[]{"Blah","Hello"}); //Emits two items
Observable<Boolean> obs3 = Observable.from(new Boolean[]{true,true}); //Emits two items

Then 1, Blah, True and 2, Hello, True would be the only items passed into the zip function(s). The item 3would never be zipped since the other observables have completed.

Bifid answered 12/2, 2016 at 12:48 Comment(4)
This wont work if one of the calls fails. In that case all calls will be lost.Grandiloquence
@Grandiloquence you can skip error by use onErrorResumeNext, example: Observable.zip(ob1, ob2........).onErrorResumeNext(Observable.<String>empty())Perfecto
What if I have a 100 observables ?Sandry
To handle error what's the best approach hereTippett
K
23

Of the suggestions proposed, zip() actually combines observable results with each other, which may or may not be what is wanted, but was not asked in the question. In the question, all that was wanted was execution of each of the operations, either one-by-one or in parallel (which was not specified, but linked Bolts example was about parallel execution). Also, zip() will complete immediately when any of the observables complete, so it's in violation of the requirements.

For parallel execution of Observables, flatMap() presented in the other answer is fine, but merge() would be more straight-forward. Note that merge will exit on error of any of the Observables, if you rather postpone the exit until all observables have finished, you should be looking at mergeDelayError().

For one-by-one, I think Observable.concat() static method should be used. Its javadoc states like this:

concat(java.lang.Iterable> sequences) Flattens an Iterable of Observables into one Observable, one after the other, without interleaving them

which sounds like what you're after if you don't want parallel execution.

Also, if you're only interested in the completion of your task, not return values, you should probably look into Completable instead of Observable.

TLDR: for one-by-one execution of tasks and oncompletion event when they are completed, I think Completable.concat() is best suited. For parallel execution, Completable.merge() or Completable.mergeDelayError() sounds like the solution. The former one will stop immediately on any error on any completable, the latter one will execute them all even if one of them has an error, and only then reports the error.

Kurtkurth answered 24/8, 2018 at 12:36 Comment(0)
P
3

With Kotlin

Observable.zip(obs1, obs2, BiFunction { t1 : Boolean, t2:Boolean ->

})

It's important to set the type for the function's arguments or you will have compilation errors

The last argument type change with the number of argument : BiFunction for 2 Function3 for 3 Function4 for 4 ...

Polygnotus answered 27/3, 2018 at 9:53 Comment(0)
C
2

You probably looked at the zip operator that works with 2 Observables.

There is also the static method Observable.zip. It has one form which should be useful for you:

zip(java.lang.Iterable<? extends Observable<?>> ws, FuncN<? extends R> zipFunction)

You can check out the javadoc for more.

Cotenant answered 12/2, 2016 at 9:1 Comment(0)
R
1

I'm writing some computation heave code in Kotlin with JavaRx Observables and RxKotlin. I want to observe a list of observables to be completed and in the meantime giving me an update with the progress and latest result. At the end it returns the best calculation result. An extra requirement was to run Observables in parallel for using all my cpu cores. I ended up with this solution:

@Volatile var results: MutableList<CalculationResult> = mutableListOf()

fun doALotOfCalculations(listOfCalculations: List<Calculation>): Observable<Pair<String, CalculationResult>> {

    return Observable.create { subscriber ->
        Observable.concatEager(listOfCalculations.map { calculation: Calculation ->
            doCalculation(calculation).subscribeOn(Schedulers.computation()) // function doCalculation returns an Observable with only one result
        }).subscribeBy(
            onNext = {
                results.add(it)
                subscriber.onNext(Pair("A calculation is ready", it))

            },
            onComplete = {
                subscriber.onNext(Pair("Finished: ${results.size}", findBestCalculation(results)) 
                subscriber.onComplete()
            },
            onError = {
                subscriber.onError(it)
            }
        )
    }
}
Rubbico answered 14/10, 2017 at 18:17 Comment(1)
not familiar with RxKotlin or @Volatile, but how would this work if this is being called by several threads at the same time? What would happen to results?Kurtkurth
M
1

I had similar problem, I needed to fetch search items from rest call while also integrate saved suggestions from a RecentSearchProvider.AUTHORITY and combine them together to one unified list. I was trying to use @MyDogTom solution, unfortunately there is no Observable.from in RxJava. After some research I got a solution that worked for me.

 fun getSearchedResultsSuggestions(context : Context, query : String) : Single<ArrayList<ArrayList<SearchItem>>>
{
    val fetchedItems = ArrayList<Observable<ArrayList<SearchItem>>>(0)
    fetchedItems.add(fetchSearchSuggestions(context,query).toObservable())
    fetchedItems.add(getSearchResults(query).toObservable())

    return Observable.fromArray(fetchedItems)
        .flatMapIterable { data->data }
        .flatMap {task -> task.observeOn(Schedulers.io())}
        .toList()
        .map { ArrayList(it) }
}

I created an observable from the array of observables that contains lists of suggestions and results from the internet depending on the query. After that you just go over those tasks with flatMapIterable and run them using flatmap, place the results in array, which can be later fetched into a recycle view.

Middelburg answered 27/10, 2019 at 0:1 Comment(0)
E
0

If you use Project Reactor, you can use Mono.when.

Mono.when(publisher1, publisher2)
.map(i-> {
    System.out.println("everything is done!");
    return i;
}).block()
Expedition answered 16/7, 2020 at 7:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.