Simplifying API consumption to Android Apps from Basic to Advanced — part3: Rx
Reactive Approach
At this point, you are capable to send and receive data in the main formats: .json, .xml. Now, let me show something that a considered an advanced issue. Look at this API Response and let’s think about how to solve it.
https://pokeapi.co/api/v2/pokemon/2/
{"species": {
"name": "ivysaur",
"url": "https://pokeapi.co/api/v2/pokemon-species/2/"
}}
After consuming the URL returned by that previous response:
https://pokeapi.co/api/v2/pokemon-species/2/
{
“base_happiness”: 50,
}
Then we have to consume that sequence of two requests. In an ideal world, one request would be enough to populate a page, on the backend, there is an option like GraphQL to avoid consumers needing to do that kind of sequence of requests, but you should not depend on this.
In theory, you could do all that you will see without any additional library, but believe me, when I say It will avoid we need to write many lines of code.
Import some libraries
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
implementation "com.squareup.retrofit2:adapter-rxjava3:2.9.0"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
API
interface Api { @GET("api/v2/pokemon/{id}/")
fun getPokemonDetail(@Path("id") id : Int = 2)
: Single<Pokemon>
}
Data Classes
data class Pokemon ( @SerializedName("species")
val specie: Specie)data class Specie (
@SerializedName("name")
val name : String,
@SerializedName("url")
val url : String,
@SerializedName("base_happiness")
val baseHappiness : Int?
)
Request
Retrofit.Builder()
.baseUrl("https://pokeapi.co/")
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(Api::class.java)
.getPokemonDetail()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ pokemon ->
println(pokemon)
})
If you did not notice, I am using a Single instead of Call. Basically, it works as an asynchronous provider of something. At this point, We have not any gain yet. But be patient, the gains are coming soon in my explanation.
addCallAdapterFactory: We have to specify this to make our retrofit instance able to return a Single instead of a Call
subscribeOn(Schedulers.io()): We are specifying our request has to be done on IO Thread
observeOn(AndroidSchedulers.mainThread()): We are specifying our request to send feedback to our main Thread
Note that baseHapinnes is still null. But this is really expected because that requires a second call.
Meet flatMap. This method requests a call after another call. This is so useful when we have to wait for a request completed and use its response to call another request.
The map method is similar, but it does a synchronous transformation. It receives a response from a request, then does there a transformation without submitting it for another asynchronous request.
val api = Retrofit.Builder()
.baseUrl("https://pokeapi.co/")
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(Api::class.java) api.getPokemonDetail().flatMap { pokemon ->
api.getSpecie(pokemon.specie.url).map { specie ->
pokemon.specie.copy(
baseHappiness = specie.baseHappiness
)
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ pokemon ->
println(pokemon)
})
Here we filled all fields, but let me explain how it happen
api.getPokemonDetail().flatMap { pokemon ->
As I told you, flatmap will call the next request, using like input the response of the previous request. The response in this case is represented by pokemon ->
Our next request will be getSpecie method but remember this provides a Specie, not a Pokemon. So here we have to use a map to do synchronous parsing. After getting a species, we will populate a field on pokemon that we obtained on the previous request, then we return that new pokemon instance that groups the content of both requests.
api.getSpecie(pokemon.specie.url).map { specie ->
pokemon.specie.copy(
baseHappiness = specie.baseHappiness
)
}
Note I used here the simplest approach. Here we have some problems like a class without a representation on the server side. We do not have a class on the server side that has a name, URL, and base_happiness.
Instead of that, there is a class that has name a and URL, and on the next step another class with base_haponess. We just merged both classes, but if you care about architecture, maybe you could adopt a different representation for each layer.
Something like:
model/entities/SpeciePage.kt
model/entities/Specie.kt
view/model/PokemonUI.kt
mapper/SpecieMapper.kt
Basically, the model layer has an exact representation that is retrieved by API. The view layer has a representation that is effectively used by UI, then we have mappers to proceed with conversions between these formats. I recommend you check my article about Clean Architecture.
Now, let’s go to a subtly more complex use case.
https://pokeapi.co/api/v2/pokemon?limit=5&offset=1
{
“results”: [
{
“name”: “ivysaur”,
“url”: “https://pokeapi.co/api/v2/pokemon/2/"
},
{
“name”: “venusaur”,
“url”: “https://pokeapi.co/api/v2/pokemon/3/"
},
{
“name”: “charmander”,
“url”: “https://pokeapi.co/api/v2/pokemon/4/"
},
{
“name”: “charmeleon”,
“url”: “https://pokeapi.co/api/v2/pokemon/5/"
},
{
“name”: “charizard”,
“url”: “https://pokeapi.co/api/v2/pokemon/6/"
}
]
}
We have here a request that returns 5 pokemon. Then when we consume each link, we receive a response like that. To be honest, this URL returns more information, but I reduced that to simplify it.
https://pokeapi.co/api/v2/pokemon/2/
{
"order": 2,
"height": 10
}
Data Classes
data class PokemonPage ( @SerializedName("results")
var results : ArrayList<Pokemon> = arrayListOf())data class Pokemon ( @SerializedName("name") var name : String? = null,
@SerializedName("url") var url : String? = null,
@SerializedName("height") var height : Int = 0)
API
interface Api {@GET("/api/v2/pokemon?limit=5&offset=1")
fun getPokemonPage(@Query("limit")
limit : Int = 5,
@Query("limit")
offset : Int = 1)
: Single<PokemonPage>fun getPokemonDetail(@Url url : String)
: Single<PokemonPage>
}
First Request
Retrofit.Builder()
.baseUrl("https://pokeapi.co/")
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(Api::class.java)
.getPokemonPage()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ pokemonPage ->
println(pokemonPage)
})
Note, our height field is still null. This is really supposed because the field requires we request another call to provide details for each pokemon.
val api = Retrofit.Builder()
.baseUrl("https://pokeapi.co/")
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(Api::class.java)api.getPokemonPage().flatMap { pokemonPage ->
val requests = pokemonPage.results.map { pkm ->
api.getPokemonDetail(pkm.url).map { detail ->
pkm.copy(height = detail.height)
}
} Single.zip(
requests
) { array ->
array.map {
it as Pokemon
}
}
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ pokemonPage ->
println(pokemonPage)
})
api.getPokemonPage().flatMap { pokemonPage ->
All inside this bracket happen after the first call is returned successfully.
Now, we will prepare the next requests. Basically, We will call one request for each item from pokemoPage (response from the first request), based on its URL.
val requests = pokemonPage.results.map { pkm ->
api.getPokemonDetail(pkm.url).map { detail ->
pkm.copy(height = detail.height)
}
}
Then after these next requests are returned successfully, synchronously we will get there the field that we want to obtain (height), and write it on the element where we obtained the URL.
val requests = pokemonPage.results.map { pkm ->
api.getPokemonDetail(pkm.url).map { detail ->
pkm.copy(height = detail.height)
}
}
At this point, we have all that we need, but I will suppose a subtle different scenario. Here, our feedback only was called after all requests be completed. For major use cases, it makes sense even if it is not the faster approach. Some developers deduce wrongly something that you request first would return first in a sequence of calls, but it is not necessarily true. So if you wish to append content to a UI list, and you care about the order, maybe would sound weirdo for a user to watch an application inserting the second item first then the first item.
But now if your feedback does not care about the ordination, follow a faster approach.
val api = Retrofit.Builder()
.baseUrl("https://pokeapi.co/")
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build().create(Api::class.java)api.getPokemonPage().toFlowable().flatMap { pokemonPage ->
val requests = pokemonPage.results.map { pkm ->
api.getPokemonDetail(pkm.url).map { detail ->
pkm.copy(height = detail.height)
}
} Single.merge(requests)
}.subscribe({ pokemon ->
println(pokemon)
})
Before this point, we used a class called Single that works fine, but it has some limitations. Basically, the Single can emit feedback once, but here we wish to call our feedback once per pokemon. Then meet Flowable (or Observable), it does what we need.
api.getPokemonPage().toFlowable()
We made a basic conversion here
Single.merge(requests)
This static method merges many Singles to produce one Flowable.
It is all, I wish I helped you consume complex APIs.