Retrofit asynchronously
This article explains how to use Retrofit asynchronously.
The problem
Download data about platforms that are said to be used by SpaceX for space travel. Use SpaceX API.
Once you have done that, download a thumbnail from Wikipedia and display it.
When the user taps on one of such platforms, they are driven to a Fragment
displaying locationof the chosen platform.
The project
The project is Launch Pads. It is also available on Google Play.
I use Retrofit 2.6.0, because it allows running asynchronous requests by adding the suspend
keyword to each function. This the dependency:
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
Credits
I originally learned how to use Retrofit in this particular manner from another blog.
The rocket icon, which I use for the launcher, Goople Play and a placeholder inside app comes from Clipart Library.
I found information on how to use Wikipedia API in a particular [thread][stackoverflow] on Stack Overflow.
Services
This is the code for downloading data from SpaceX API and Wikipedia API:
interface SpaceXService {
@GET("launchpads")
suspend fun all(): Response<List<LaunchPadJson>>
}
class SpaceXServiceFactory {
fun getLaunchPadService(): SpaceXService = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(SpaceXService::class.java)
companion object {
const val BASE_URL = "https://api.spacexdata.com/v3/"
}
}
interface WikipediaService {
@GET("summary/{article}")
suspend fun getSummary(@Path("article") article: String): Response<WikipediaArticle>
}
class WikipediaServiceFactory {
fun getWikipediaService(): WikipediaService = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(WikipediaService::class.java)
companion object {
const val BASE_URL = "https://en.wikipedia.org/api/rest_v1/page/"
}
}
These are the Kodein bindings:
bind<SpaceXServiceFactory>() with singleton { SpaceXServiceFactory() }
bind<SpaceXService>() with singleton { spaceXServiceFactory.getLaunchPadService() }
bind<WikipediaServiceFactory>() with singleton { WikipediaServiceFactory() }
bind<WikipediaService>() with singleton { wikipediaServiceFactory.getWikipediaService() }
Downloading and parsing
This is the type of the data downloaded from SpaceX API:
@ExperimentalCoroutinesApi
data class LaunchPadJson(
val status: String,
val location: Location,
val wikipedia: String,
val site_id: String) {
fun toLaunchPad(scope: CoroutineScope) = LaunchPad(
status,
location,
scope.async { getThumbnail() },
site_id
)
private suspend fun getThumbnail(): String {
val response = wikipediaService.getSummary(wikipedia.replace(PREFIX, ""))
return if (response.isSuccessful) response.body()!!.thumbnail.source else ""
}
companion object {
const val PREFIX = "https://en.wikipedia.org/wiki/"
}
}
It contains a function converting it to another format. Please note that the resulting class contains one Deferred
field:
@ExperimentalCoroutinesApi
data class LaunchPad(val status: String, val location: Location, val thumbnail: Deferred<String>, val site_id: String)
This is the way it is used. Please note that it can be run in two modes. At first, when the Deferred
hasn’t completed, it simply calls await()
on it. The second times it calls getCompleted()
, that can be called withouth suspension, but throws IllegalStateException
if the data isn’t yet available:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val lp = list[position]
holder.name.text = lp.location.name
holder.status.text = lp.status
try {
Picasso.get()
.load(lp.thumbnail.getCompleted())
.into(holder.thumbnail)
}
catch (e: IllegalStateException) {
scope.launch {
Picasso.get()
.load(lp.thumbnail.await())
.into(holder.thumbnail)
}
}
}
Initially I was trying not to use getCompleted()
at all, which forced me to each time start a coroutine and call await()
in it. However, this resulted in the placeholder being displayed ever-so-briefry each time when I returned to this Fragment
from another one; I concluded therefore that I had to call launch()
inside onBindViewHolder()
very sparingly.
The LiveData
This is the way I create the LiveData
that is going to be observed:
val ld = liveData(
context = viewModelScope.coroutineContext,
timeoutInMs = Long.MAX_VALUE) {
coroutineScope {
val response = spaceXService.all()
if (response.isSuccessful) {
emit(Status.Success(response.body()!!.map { it.toLaunchPad(this) }))
}
else {
emit(Status.Error)
}
}
}
sealed class Status {
object Error : Status()
data class Success(val list: List<LaunchPad>) : Status()
}
Because of the above syntax I get the following:
- The download only starts when the data starts being observed. It is therefore the equivalent of RxJava’s
doOnSubscribe()
, which only starts the execution when there is a subscriber. - The code, however, does not end when the last observer unsubscribes, so it will not delete the downloaded data when the
Fragment
enters paused stade. The developer may control the behavior by modifying the value set totimeoutInMs
. The value set to it indicates the timeout that has to pass afer the last observer is removed (for instance when screen is rotated or application is paused), without adding new observes, for the data contained therein do be discarded. - The download is canceled and the already downloaded data, if any, is discarded, when
viewModelCoroutineContext
is canceled.
The GUI
This is the way the above LiveData
is used with the GUI:
(list.ld to this) { response ->
if (response is ListVM.Status.Success) {
progress.visibility = View.GONE
recycler.visibility = View.VISIBLE
recycler.verticalDivider()
recycler.adapter = LaunchPadAdapter(response.list, lifecycleScope) { position ->
single.launchPad = response.list[position]
nav(R.id.action_mainFragment_to_mapFragment)
}
recycler.layoutManager = LinearLayoutManager(context)
}
else {
getString(R.string.launch_pads_network_error).toast()
}
}
The code does use some DSL, but I hope it is fine with the reader, as the DSL parts have been described previously in this blog. In either case, the reader is welcome to investigate the code further by cloning my project to their local machine.
The map
Clicking on each item displayed in the RecyclerView
does lead the user to a Fragment
with Google Maps presenting its location, but discussing the code that does it outside of the scope of the present article. It doesn’t really matter how I do it. I only used navigation to another Fragment
as a way of demonstrating what happens to the contents of the original Fragment
when it is hidden.
Displaying Google Maps has already been discussed thoroughly in another article in this blog.
Conclusion
The article has shown how to solve common problems in asynchronous data loading:
- How to download a JSON, and immediately start downloading individual JSONs per every item contained in the original one.
- Where to store such data while it is still being downloaded. For this I used
Deferred
. - How to use Picasso with such
Deferred
. How to prioritize using it without suspending onawait()
to avoid briefly showing the placeholder unnecessarily.
I tried to demonstrate thru this article how to simultaneously use three levels of asynchronous data downlading: Two levels with Retrofit and one with Picasso.
Donations
If the reader has enjoyed the present article, they may want to donate some bitcoin at the address presented below. Readers may also look at my donations page.
BTC: bc1qncxh5xs6erq6w4qz3a7xl7f50agrgn3w58dsfp