RxJava: chain API1 -> API2 call and get API1 result

How can we chain the APIs and get result from one of the APIs?

Today let’s talk about a scenario: we’d like to implement a new feature to publish a new post on our app, and we have to follow the flow:

Technical Flow

  1. Make a API1 request to create media ID.
  2. Get the media upload URL from location header of API1.
  3. Upload file to that media upload URL.
  4. Store the media ID to the media.
  5. Wait for push notification with media ID to confirm that the job on server side for that media is done.
Media Upload.png
Flow Chat

API Request/Response

The first API to create media ID and get media upload URL:

POST /test/api1
Authorization Bearer JWT
User-Agent: Make sure to include platform
---
200 OK
Content-Type: application/json
Location: MEDIA_UPLOAD_URL
{
"media_id": "UUID4-STRING"
}
view raw Api1 hosted with ❤ by GitHub

The second API to upload media:

POST /test/api2
Authorization Bearer JWT
User-Agent: Make sure to include platform
Content-Type: image/jpeg
Content-Length: 7351375
Body: file
---
202 Accepted
view raw Api2 hosted with ❤ by GitHub

Problem to solve?

You need to call two APIs (or more) sequentially and chain together, both the two APIs success, then the publish flow successes, once one of the APIs fail, then the whole flow is considered to be failed.  After the both API calls, we have to get the post ID from API1, how can we chain the APIs and get result from the first API?

Here we use RxJava to help us solve this problem, and use Retrofit and Okhttp for API call, let’s convert each API requests into several standalone Observable: (for clear and convenient, we choose API1, API2, … to represent the first, second… API than the real production name)

1. We define the data class for API response first, this data class to store the response from API1:

data class Media(@SerializedName("media_id") val mediaId: String)
view raw Media.kt hosted with ❤ by GitHub

2. The two APIs observables:

object Api {
fun api1(): Observable<Response<Media>> =
ApiHelper.getApi().api1()
fun api2(uploadUrl: String): Observable<okhttp3.Response> =
Observable.fromCallable {
val request = Request.Builder()
.url(uploadUrl)
.build()
ApiHelper.getMediaUploadHttpClient().newCall(request).execute()
}
}
view raw Api.kt hosted with ❤ by GitHub

3. Use RxJava.flapMap() to chain the API1 -> API2:

flatMap.c.png
Source: http://reactivex.io/documentation/operators/flatmap.html
fun chainApi1AndApi1(): Observable<okhttp3.Response> {
return Api.api1().flatMap { response1 ->
if (response1.isSuccessful) {
var uploadUrl = response.headers().get(ApiUtils.Header.LOCATION)
uploadUrl?.let { url ->
Api.api2(url)
} ?: kotlin.run {
throw Exceptions.propagate(NullPointerException("Media upload URL is empty"))
}
} else
Observable.error(ApiRequestException(response1))
}
}
view raw ChainApi.kt hosted with ❤ by GitHub

And its subscriber:

chainApi1AndApi1()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Subscriber<okhttp3.Response>() {
override fun onCompleted() {
}
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onNext(response: okhttp3.Response) {
if (response.isSuccessful)
Logger.d(response)
else
onError(ApiRequestException(response))
}
})

This is the common  way to chain two API calls by using flatMap()  You can find that the return type in subscriber is the same type from the last observable, i.e.okhttp3.Response, it doesn’t include the media ID from API1, however, we need the media ID from API1 after API2, what can we do to achieve that then?

Solution1

We can simple use map() operator to apply a pair function to map the media ID from API1 and API2 together:

fun zipApi1AndApi2(): Observable<Pair<Media, okhttp3.Response>> {
return Api.api1().flatMap { response1 ->
if (response1.isSuccessful) {
var uploadUrl = response1.headers().get(ApiUtils.Header.LOCATION)
uploadUrl?.let { url ->
Api.api2(url).map { response2 -> Pair(response1, response2) }
} ?: run {
throw Exceptions.propagate(NullPointerException("Media upload URL is empty"))
}
} else
Observable.error(ApiRequestException(response1))
}
}
view raw ChainApi1.kt hosted with ❤ by GitHub

And its new subscriber can get the pair of media ID and okhttp3.Response:

zipApi1AndApi2()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { pair ->
if (pair.second.isSuccessful)
Logger.d(pair)
else
throw ApiRequestException(pair.second)
}

Solution2

We can use zip() operator to combine the media ID from API1 along with API2, that is api1.flatmap -> zip(api1Result, api2) , the original implementation above is api1.flatmap -> api2:

zip.jpg
Source: http://reactivex.io/documentation/operators/zip.html
fun zipApi1AndApi2(): Observable<Pair<Media, okhttp3.Response>> {
return Api.api1().flatMap { response ->
if (response.isSuccessful) {
var uploadUrl = response.headers().get(ApiUtils.Header.LOCATION)
uploadUrl?.let {url ->
Observable.zip(
Observable.just(response.body()),
Api.api2(url)
) { t1, t2 -> Pair(t1, t2) }
} ?: run {
throw Exceptions.propagate(NullPointerException("Media upload URL is empty"))
}
} else
Observable.error(ApiRequestException(response))
}
}
view raw ChainApi2.kt hosted with ❤ by GitHub

Solution3

We also can use cache() to help alternatively.

replay.c.png
Source: http://reactivex.io/documentation/operators/replay.html

The operator order is api1.cache.flatmap(api2.zip(api1))

fun cacheApi1AndZipApi2(): Observable<Pair<Media, okhttp3.Response>> {
val api1: Observable<Response<Media>> = Api.api1().cache()
return api1.flatMap { response ->
if (response.isSuccessful) {
var uploadUrl = response.headers().get(ApiUtils.Header.LOCATION)
uploadUrl?.let { url ->
Api.api2(url).zipWith(api1) { r2, r1 -> Pair(r1.body(), r2) }
} ?: run {
throw Exceptions.propagate(NullPointerException("Media upload URL is empty"))
}
} else
Observable.error(ApiRequestException(response))
}
}
view raw ChainApi3.kt hosted with ❤ by GitHub

There are still some variants for this use case, you can find it out by yourself, happy coding!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑