1
votes

I'm trying to get myself into RxJava so I read some posts about it. I think I understood how it works but I would like to submit you a theorical code to ensure my good understanding of the library.

Let's imagine I have an API enabling to retrieve a list of produceur and for each of them the artists they produce, their albums and their songs. My model would be the following

public class Produceur {
    private Integer id;
    private String name;
    private String pictureUrl;
}

public class Artist {
    private Integer id;
    private String name;
    private String lastname;
    private String sceneName;
    private String pictureUrl;
}

public class Album {
    private Integer id;
    private int year;
    private String name;
    private String style;
    private String pictureUrl;
    private Integer artistId;
}

public class Song {
    private Integer id;
    private String title;
    private int duration;
    private String pictureUrl;
    private Integer albumId;
}

With my Retrofist services

@GET("myUrl/produceurs")
Observable<List<ProduceurResponse>> getProduceurs();

@GET("myUrl/produceurs/{produceurId}")
Observable<List<ArtisteResponse>> getArtistForProduceur(@Path("produceurId") Integer produceurId);

@GET("myUrl/picture/{id}")
Observable<ResponseBody> getPicture(@Path("id") Integer id);

And the response objects

public class ProduceurResponse {
    private Integer id;
    private String name;
    private String pictureUrl;
}

public class ArtisteResponse {
    private Integer id;
    private String name;
    private String lastname;
    private String sceneName;
    private String pictureUrl;
    private List<AlbumResponse> albums;
}

public class AlbumResponse {
    private Integer id;
    private int year;
    private String name;
    private String style;
    private String pictureUrl;
    private Integer artistId;
    private List<SongResponse> songs;
}

public class SongResponse {
    private Integer id;
    private String title;
    private int duration;
    private String pictureUrl;
    private Integer albumId;
}

I would like to retrieve all the produceur and for each the artists and save all the images in the local memory in the same sequence. I thought about the following code where for each produceur we will retrieve the artists, their albums and song and insert them into our base. And once we've finished retrieveing the produceurs and artists we can download images (we stored in a list every id with a picture).

getProduceurs().flatMap(produceurs -> Observable.from(produceurs))
               .doOnNext(produceur -> insertProduceurInBase(produceur))
               .subscribe(produceur -> Observable.from(getArtistForProduceur(produceur.getId())
                                                 .flatMap(artists -> Observable.from(artists))
                                                 .doOnNext(artist -> insertArtistInBase(artist)))
                                                 .subscribe(),

                          e -> showError(e),

                          () -> Observable.from(listOfIdsToDownload)
                                          .doOnNext(id -> getPicture(id))
                                          .subscribe(response -> createImage(response),

                                          e -> showError(e),

                                          () -> isFinished()
                                          )
                         );

Would this code work (I think so but I'm not sure) ? Is this the best way to do it ?

Now, what if my getProduceurs service return me a list of ProduceurResponse containing produceur artists ids and I got a service to retrieve the artist profil and another one to retrieve its albums.

public class ProduceurResponse {
    private Integer id;
    private String name;
    private String pictureUrl;
    List<Integer> artistsIds;
}

public class ArtisteProfileResponse {
    private Integer id;
    private String name;
    private String lastname;
    private String sceneName;
    private String pictureUrl;
}

@GET("myUrl/artist/{artistId}")
Observable<List<ArtisteProfileResponse>> getArtistProfile(@Path("artistId") Integer artistId);

@GET("myUrl/artist/{artistId}/detail")
Observable<List<AlbumResponse>> getArtistAlbums(@Path("artistId") Integer artistId);

I could to use the .zip to make the getArtistProfile() and getArtistAlbums() call simultaneous with something like

getProduceurs().flatMap(produceurs -> Observable.from(produceurs))
               .doOnNext(produceur -> insertProduceurInBase(produceur))
               .subscribe(produceur -> Observable.from(produceur.getArtistIds())
                                                 .zip(
                                                        getArtistProfile(),
                                                        getArtistAlbums(),
                                                        (artistProfil, albumList) -> insertArtistInBase()
                                                 )
                                                 .subscribe(),

                          e -> showError(e),

                          () -> Observable.from(listOfIdsToDownload)
                                          .doOnNext(id -> getPicture(id))
                                          .subscribe(response -> createImage(response),

                                          e -> showError(e),

                                          () -> isFinished()
                                          )
                         );

But I'm really not sure I'm using the zip the right way. Is zip well used in this piece of code ? Would this work ? Is this the best way to do it ?

EDIT

So I tried to implement something similar to my original idea with the google books api.

I've got a Retrofit interface

public interface IBookService {
    @GET("volumes?q=robot+subject:fiction")
    Observable<BookSearchResult> getFictionAuthors(@Query("category") String key);
    @GET("volumes")
    Observable<BookSearchResult> getBooksForAuthor(@Query("q") String author, @Query("category") String key);
}

With

public class BookSearchResult {
    public List<BookResult>  items;
}

public class BookResult {
    public String id;
    public String selfLink;
    public VolumeInfoResult volumeInfo;
    public SaleInfoResult saleInfo;
}

And I try to retrieve the books of fiction with a the string robot (getFictionAuthors) with return me a BookSearchResult containing a list of BookResult. For each of the book results I retrieve all the books of the author with getBooksForAuthor. My code is below

Observable<BookResult> observable = mWebService.getFictionAuthors(API_KEY)
                .flatMap(new Func1<BookSearchResult, Observable<BookResult>>() {

                    // Parse the result and build a CurrentWeather object.
                    @Override
                    public Observable<BookResult> call(final BookSearchResult data) {
                        return Observable.from(data.items);
                    }
                })
                .concatMap(new Func1<BookResult, Observable<BookSearchResult>>() {

                    // Parse the result and build a CurrentWeather object.
                    @Override
                    public Observable<BookSearchResult> call(final BookResult data) {
                        return mWebService.getBooksForAuthor("=inauthor:" + data.volumeInfo.authors.get(0), API_KEY);
                    }
                })
                .flatMapIterable(new Func1<BookSearchResult, List<BookResult>>() {

                    // Parse the result and build a CurrentWeather object.
                    @Override
                    public List<BookResult> call(final BookSearchResult data) {
                        return data.items;
                    }
                })
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<BookResult>() {
                    @Override
                    public void onNext(final BookResult book) {
                        Log.e("Book","Book is " + book.volumeInfo.title + " written by " + book.volumeInfo.authors.get(0));
                    }

                    @Override
                    public void onCompleted() {
                        Log.e("Book","Book list completed");
                    }

                    @Override
                    public void onError(final Throwable error) {
                        Log.e("Book","Book list error");
                    }
                }

This code is working but there is something weird that I don't understand. In my logs I have at first the return from the getBooksForAuthor request for the first author and then the log from each book of that author. After that I've got the result of the request for the second author and part of the log from his books. Follow the results of the request of the other authors and then the end of the list of books for the second author and the list of books for all the other authors.

To illustrate it, my logs looks like

- > Return from request for Author 1
- > Book 1 from author 1
...
- > Book 10 from author 1
- > Return from request for Author 2
- > Book 1 from author 2
...
- > Book 5 from author 2
- > Return from request for Author 3
- > Return from request for Author 4
...
- > Return from request for Author 10
- > Book 6 from author 2
..
- > Book 10 from author 2
- > Book 1 from author 3
..
- > Book 10 from author 3
...
- > Book 1 from author 10
..
- > Book 10 from author 10

When I expected

- > Return from request for Author 1
- > Book 1 from author 1
...
- > Book 10 from author 1
- > Return from request for Author 2
- > Book 1 from author 2
...
- > Book 10 from author 2
...
- > Return from request for Author 10
- > Book 1 from author 10
...
- > Book 10 from author 10

Does someone have an explanation or understand what I'm missing ?

1
what exactly you want, like to want to save the images in sequences right? - Raut Darpan
I would like to retrieve all my data and save my images in the same sequence. What I have in mind is that I log in, my app retrieve all the data I need and then I can use the app. - Tibo
rxjava has this operators : merge() - which gets data in sequence that it receives from api. buffer() : this operator creates buffer so that there is no load. you can try this - Raut Darpan
you have to create a BaseActivity to do all Api calls so that you can use them - Raut Darpan
From what I understood of merge, its purpose is to combine Observables into a single Observable. So it might replace the zip but I don't figure out what else I could do with it. Did I misunderstood the merge function ? - Tibo

1 Answers

2
votes

You should avoid nested subscription (check out the flatmap operator). It's offen an indicator of a code smell. To avoid nested subscription, you can use the operator flatMap (do not garanti resulting events order) or concatMap (garanti resulting events order).

I notice too that you're using another Observable in the completed callback : you can concat observables in this case.

So the rework of your code using this sort of operators :

 getProduceurs().flatMap(produceurs -> Observable.from(produceurs))
                .doOnNext(produceur -> insertProduceurInBase(produceur))
                // call getArtistForProduceur and emit results in order
                .concatMap(produceur -> getArtistForProduceur(produceur.getId()))
                // emits items of the list
                .flatMapIterable(artists -> artists)
                .doOnNext(artist -> insertArtistInBase(artist)))
                // don't care about elements. But will wait for the completion of the previous observable
                .ignoreElements()
                // perform jobs after the previous observable complete
                .concatWith(Observable.from(listOfIdsToDownload)
                                      .doOnNext(id -> getPicture(id))
                                      .doOnNext(response -> createImage(response)))
                // show an error if an error occur in the downstream
                .doOnError(e -> showError(e))
                // call isFinished when everything is finished.
                .doOnCompleted(() -> isFinished())
                .subscribe()