Android : Handle Network Requests with Kotlin Coroutines and Retrofit

Anubhav
5 min readJul 5, 2021
Photo by Kvistholt Photography on Unsplash

While working on production level android applications, we come across scenarios where we need to make multiple network requests which can be either be chained requests or paralleled requests and we might want to combine their results as well. In this article we will see how to handle the mentioned scenarios using Kotlin Coroutines.

Brief Overview of Coroutines

A coroutine is a concurrency design pattern that we can use in Android to simplify code that executes asynchronously. A coroutine is an instance of suspendable computation. It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. Suspending functions are at the centre of everything coroutines. A suspending function is simply a function that can be paused and resumed at a later time. They can execute a long running operation and wait for it to complete without blocking.

As per the Kotlin official documentation,

Coroutines simplify asynchronous programming by putting the complications into libraries. The logic of the program can be expressed sequentially in a coroutine, and the underlying library will figure out the asynchrony for us.

Features of Coroutines

  • Lightweight
  • Fewer memory leaks
  • Built-in cancellation support
  • Jetpack integration

In this article, we will see how to use Kotlin Coroutine to

  • Chain multiple network requests
  • Create parallel network requests and process the responses when all the requests have finished.

For our implementation, we need to understand two Kotlin Coroutines functions,

  • launch {} : It launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job. Conceptually, a job is a cancellable thing with a life-cycle that culminates in its completion. The coroutine is cancelled when the resulting job is cancelled.
  • async {} : Creates a coroutine and returns its future result as an implementation of Deferred. Deferred value is a non-blocking cancellable future, it is a Job with a result.The running coroutine is cancelled when the resulting deferred is cancelled.

Interestingly enough, Deferred extends Job. This implies Deferred must be providing additional functionality on top of Job. Deferred is type parameterised over where T is the return type. Thus, Deferred object can return some response from the block of code executed by async method.

A simple launch code block will look like,

viewModelScope.launch {
delay(1000)
println("From launch block")
}

A simple async code block will look like,

val result: Deferred<String> = viewModelScope.async {
delay(1000)
"From Async Block"
}

println(result.await())

When we do not care about the task’s return value, and just want to execute it, we may use Launch. If we need the return type from the task/coroutine we should use async.

CoroutineScope

A CoroutineScope defines a lifecycle, a lifetime, for coroutines that are built and launched from it. A CoroutineScope lifecycle starts as soon as it is created and ends when it is canceled or when it’s associated Job or Supervisor Job finishes. When that happens, the CoroutineScope is no longer active.

Any launch{} or async{} coroutine built from a CoroutineScope, if it is still running, will be canceled when its CoroutineScope lifecycle ends.

Android “androidx.lifecycle:lifecycle-viewmodel-ktx:$VERSION” provides an easy way to get a CoroutineScope in the Android ViewModel.

Enough of the theory, let’s get to the implementation,

I will be using the Pokemon API for our demo. We will have two model response types, PokemonResponse and Pokemon. PokemonResponse class contains a list of results, where each result has a url, which in turn returns a response of type Pokemon.
Our API Interface will look like,

interface PokemonService {

@GET("pokemon")
suspend fun getAllPokemon(): Response<PokemonResponse>

@GET("pokemon/{id}")
suspend fun getPokemonDetails(@Path("id") id: String): Response<Pokemon>

}

Our API Interface’s functions are suspend functions so that they can be invoked from another suspend function or from a coroutine block for asynchronous calls. The remote data source will look like,

class RemoteDataSource @Inject constructor(private val pokemonService: PokemonService) {

suspend fun getAllPokemon() =
pokemonService.getAllPokemon()

suspend fun getPokemonDetails(id: String) =
pokemonService.getPokemonDetails(id)

}

Despite not having any local data source, we will be creating a remote data source, to follow the practice where the ViewModel obtains the data from the repository, which in turn fetches the data from remote and local data sources. To get an understanding of the practice I generally follow in my articles please have a look at the following article.

Our Repository will look like,

@ActivityRetainedScoped
class Repository @Inject constructor(
private val remoteDataSource: RemoteDataSource,
@ApplicationContext context: Context
) : BaseApiResponse(context) {

suspend fun getAllPokemon(): Flow<NetworkResult<PokemonResponse>> {
return flow {
emit(safeApiCall { remoteDataSource.getAllPokemon() })
}.flowOn(Dispatchers.IO)
}

suspend fun getPokemonDetails(id: String): Flow<NetworkResult<Pokemon>> {
return flow {
emit(safeApiCall { remoteDataSource.getPokemonDetails(id) })
}.flowOn(Dispatchers.IO)
}
}

Now we will be using these methods in our ViewModel class to obtain each pokemon’s image url.

Chaining Network Requests

To obtain a Pokemon, we need to get the result of PokemonResponse. This is where we will understand how to chain network requests.

fun getAllPokemonResponse() = viewModelScope.launch {
repository.getAllPokemon()
.collect { values ->
responseType.value = NetworkResult.Loading()
_pokemonResponse.value = values
}
_pokemonResponse.value
?.data?.let {
for (result in it.results) {
val id = getId()
repository.getPokemonDetails(id).collect { pokemon ->
pokemon.data?.let {
pokemonDetailsList.add(pokemon.data)
}
}
}
}
_pokemonList.value = pokemonDetailsList
}

Your implementation can vary, this is an example of how you can chain requests using Coroutines.

Parallel Network Requests

Suppose we want to fetch multiple Pokemon details to compare their strengths, we will be making multiple getPokemonDetails(id) requests in parallel.

fun getPokemonInfo() = viewModelScope.launch {
try {
val pokemon1 = async { repository.getPokemonDetails(ID_1) }
val pokemon2 = async { repository.getPokemonDetails(ID_2) }

proceedWithResponses
(pokemon1.await().asLiveData(), pokemon2.await().asLiveData())
} catch (e: Exception) {
Log.e("TAG", "getParallelPokemon: ${e.message}")
}
}

Both the requests in the above code snippet happen asynchronously, and proceedWithResponses will only be called when both pokemon1 and pokemon2 are finished. This is a demo of making network requests in parallel, the implementation will depend on how you have structured your response types.

And we are done !

The code snippet for the same can be found here, though there is no example of parallel requests in my current implementation.

Hope you learnt something today, Cheers !

--

--