0

I'm trying to do a parallel download of a list of images, combining them to a map.

At first I tried to make an Observable like this:

Observable<Map<Integer, Bitmap>> getImages(final List<Activity> activities) {
    return Observable.create(new Observable.OnSubscribe<Map<Integer, Bitmap>>() {
        @Override
        public void call(Subscriber<? super Map<Integer, Bitmap>> subscriber) {
            try {
                Map<Integer, Bitmap> result = new HashMap<Integer, Bitmap>();
                for (Activity act : activities) {
                    result.put(act.getId(), downloadImage(act.getImage()));
                }
                subscriber.onNext(result);
                subscriber.onCompleted();
            } catch (Exception e) {
                subscriber.onError(e);
            }
        }
    });
}

This works, but it's not what I want. Because the images are being downloaded sequentially. And I think for-loops are not nice in rxjava. So I created these Observables:

    Observable<Bitmap> getImage(final Activity activity) {
    return Observable.create(new Observable.OnSubscribe<Bitmap>() {
        @Override
        public void call(Subscriber<? super Bitmap> subscriber) {
            try {
                subscriber.onNext(downloadImage(activity.getImage()));
                subscriber.onCompleted();
            } catch (Exception e) {
                subscriber.onError(e);
            }
        }
    });
}

Observable<Map<Integer, Bitmap>> getImages(final List<Activity> activities) {
    return Observable
        .from(activities)
        .flatMap(new Func1<Activity, Observable<Bitmap>>() {
            @Override
            public Observable<Bitmap> call(Activity activity) {
                return getImage(activity);
            }
        })
        .toMap(new Func1<Bitmap, Integer>() {
            @Override
            public Integer call(Bitmap bitmap) {
                return 1; // How am I supposed to get the activity.getId()?
            }
        });
}

So I made an Observable for getting a single image, trying to combine them in the second one using flatMap. This works, but there are still 2 problems:

  1. When I do a toMap(), how can I retrieve the right id to use as key for the map? I want to use the Activity object for that.
  2. Unfortunately, the downloads are still beging processed sequentially and not in parallel. How can I fix this?
RuudJ
  • 53
  • 8

3 Answers3

2

Create a wrapper class that hold a bitmap and an Activity. Say ActivityBitmap. Replace getImage with getActivityBitmap:

Observable<ActivityBitmap> getActivityBitmap(final Activity activity) {
    return Observable.create(new Observable.OnSubscribe<ActivityBitmap>() {
        @Override
        public void call(Subscriber<? super ActivityBitmap> subscriber) {
            try {
                subscriber.onNext(new ActivityBitmap(activity, downloadImage(activity.getImage())));
                subscriber.onCompleted();
            } catch (Exception e) {
                subscriber.onError(e);
            }
        }
    });
}

and call it like below. Note that to get asynchronous downloads you use subscribeOn in the flatMap. To build the Map<Integer,Bitmap> at the end you use a different overload of toMap that allows you to specify key and value.

Observable<Map<Integer, Bitmap>> getImages(final List<Activity> activities) {
    return Observable
        .from(activities)
        .flatMap(new Func1<Activity, Observable<ActivityBitmap>>() {
            @Override
            public Observable<ActivityBitmap> call(Activity activity) {
                return getActivityBitmap(activity).subscribeOn(Schedulers.io());
            }
        })
        .toMap(new Func1<ActivityBitmap, Integer>() {
            @Override
            public Integer call(ActivityBitmap activityBitmap) {
                return activityBitmap.getActivity().getId();
            }
        },new Func1<ActivityBitmap, Bitmap>() {
            @Override
            public Integer call(ActivityBitmap activityBitmap) {
                return activityBitmap.getBitmap();
            }
        });
}
Dave Moten
  • 11,957
  • 2
  • 40
  • 47
1

I have a possible solution. It uses the reduce operator to convert to the map. Though, I'm not sure that subscribing to an Observable inside an Observable is good practice.

Observable<Bitmap> getImage(final Activity activity) {
    return Observable.create(new Observable.OnSubscribe<Bitmap>() {
        @Override
        public void call(Subscriber<? super Bitmap> subscriber) {
            try {
                subscriber.onNext(downloadImage(activity.getImage()));
                subscriber.onCompleted();
            } catch (Exception e) {
                subscriber.onError(e);
            }
        }
    });
}

Observable<HashMap<Integer, Bitmap>> getImages(final List<Activity> activities) {
    return Observable
        .from(activities)
        .reduce(new HashMap<Integer, Bitmap>(), new Func2<HashMap<Integer, Bitmap>, Activity, HashMap<Integer, Bitmap>>() {
            @Override
            public HashMap<Integer, Bitmap> call(final HashMap<Integer, Bitmap> bitmaps, final Activity activity) {
                getImage(activity)
                    .observeOn(Schedulers.io())
                    .subscribeOn(Schedulers.io())
                    .subscribe(new Action1<Bitmap>() {
                        @Override
                        public void call(Bitmap bitmap) {
                            bitmaps.put(activity.getId(), bitmap);
                        }
                    });
                return bitmaps;
            }
        });
}

Would really appreciate feedback on this solution.

RuudJ
  • 53
  • 8
0

I would go like that:

Observable<Map<Integer, Bitmap>> getImages(List<Activity> activities) {
    return Observable.from(activities)
          .map(activity -> new Pair(activity.getId(), downloadImage(activity.getImage())))
          .toMap(pair -> pair.first, pair -> pair.second);
}

(nota: I use retrolambda, hence the lambdas)

Apparently, something like that should be parallel:

Observable.from(activities)
          .flatMap(activity ->
                Observable.zip(Observable.just(activity.getId(),
                               downloadImage(activity.getImage())),
                (k, v) -> new Pair(k, v)))
          .toMap(pair -> pair.first, pair -> pair.second);

(Provided that downloadImage returns an asynchronous Observable)

njzk2
  • 38,969
  • 7
  • 69
  • 107
  • Elegant solution, but this still doesn't seem to run in parallel. Is there a way to fix that with this construction? – RuudJ Jul 07 '15 at 15:31
  • the problem is that `map` is expected to return a sequence in the same order as the input – njzk2 Jul 07 '15 at 15:34
  • may be `zip` could help here. possibly a `reduce` with a `zip`? – njzk2 Jul 07 '15 at 15:40
  • also, see http://stackoverflow.com/questions/26249030/rxjava-fetching-observables-in-parallel – njzk2 Jul 07 '15 at 15:42