Search code examples
rx-javasystem.reactiverx-kotlin

RXJava/Kotlin - Chaining Single results in one


I have a problem and I don't know how to resolve it with a better aproach. The problem is that I'm requesting to Spotify Web API and in some methods the artist image is returned and in others only basic artist info is obtained.

I have this two methods:

fun getAlbum(albumId: String): Single<SpotifyAlbumDTO>

fun getArtist(artistId: String): Single<SpotifyArtistDTO>

When I get an album, the artist info doesn't contains the artist image url. So, I need to call the getAlbum() method and use the result to obtain the artistId and then call to getArtist() method.

I have the following method to do all this stuff:

fun getAlbum(albumId: String): Single<Album>

On this method I need to call the previous two to return an Album object (my domain object). The only solution that worked for me it's the following:

fun getAlbum(albumId: String): Single<Album> {
  return Single.create { emitter ->
    _spotifyService.getAlbum(albumId).subscribe { spotifyAlbum ->
      _spotifyService.getArtist(spotifyAlbum.artist.id).subscribe { spotifyArtist ->
        val artistImage = spotifyArtist.imageUrl
        spotifyAlbum.artist.image = artistImage
        emitter.onNext(spotifyAlbum.toAlbum())
      }
    }
  }
}

I think that must exist another better way to do this than concating subscribe calls in other subscribes and creating deeper calls. I also try the following:

_spotifyService.getAlbum(albumId).flatMap { spotifyAlbum ->
  _spotifyService.getArtist(spotifyAlbum.artist.id)
}.flatMap { spotifyArtist ->
  // Here I don't have the album and I can't to asign the image         
}

Solution

  • PARALLEL SOLUTION

    To combine several source of data the best operator is zip:

    Rx Zip operator

    Single.zip(getAlbum(albumId), getArtist(artistId),
        BiFunction<SpotifyAlbumDTO, SpotifyArtistDTO, SpotifyAlbumDTO> { album, artist -> 
            val artistImage = artist.imageUrl
            album.artist.image = artistImage
            album //return value of the merged observable 
        }
    ).subscribe { album: SpotifyAlbumDTO?, error: Throwable? ->
        emitter.onNext(album.toAlbum())
    }
    

    It will run all the observables in parallel, and execute the merging function once every observable has finished.

    If you have more observable to zip, you can use Function3, Function4,...

    SEQUENTIAL SOLUTION (EDIT)

    If parallel execution is not possible because you need the request to be executed sequentially then, you can you the resultSelector of the flatmap function. It takes the item before the flatMap and the grouped collection after the flatmap. This way you will easily be able to create your model group, without any confusing usage of Pair

    Flatmap with resultSelector

    The only catch is: Single does not support this kind of flatmap. You can workaround the issue either changing your return type from Single to Observable or just convert your Single at runtime with the toObservable operator.

    getAlbum(albumId)
        .toObservable()
        .flatMap({album: SpotifyAlbumDTO -> getArtist(album.artist.id).toObservable()},
                 { album:SpotifyAlbumDTO, artist:SpotifyArtistDTO ->
                     album.artist.image = artist.imageUrl
                     album
                 })
        }?.subscribe { album: SpotifyAlbumDTO ->
            print(album)
        }