Handle Complex Network Call on Android with Kotlin Coroutine + Retrofit 2 in MVVM

In this article, I’ll demonstrate how to use coroutine in order to - Chain multiple network request - Create parallel network requests and process the responses when all of them are finished.

When you’ve been working with some Android applications, you may encounter the scenario where you have to make multiple network requests such as chained or parallel requests and combine their results.

To handle the mentioned scenario, some might say “let’s use RxJava!!!” but… come on the learning curve of RxJava is no joke, at least for me.

:)

Don’t get me wrong, it doesn’t mean that I hate RxJava. RxJava is a really powerful library but Kotlin coroutine seems to suit my existing app architecture better.

In this article, I’ll demonstrate how to use coroutine in order to
  • Chain multiple network request
  • Create parallel network requests and process the responses when all of them are finished.

I won’t get into much detail about how coroutine works but I’ll focus on using it in action.

Introduction

Before we begin I would like to briefly address the concept and the commonly used functions in Coroutine.

Brief Concept

Writing the code asynchronously has always been a pain for me, especially when dealing with complex network requests. It’s hard to write easy-to-read code and sometimes you may end up with a lot of callbacks in your code. Therefore, one of the purposes of Kotlin Coroutine is to help us write a neater code when working with asynchronous programming.

Here is what written in Kotlin Official document.

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.

Launch and Async

launch and async are the most commonly used  Coroutine builder.

Here is the official definition.

launch- Launches new coroutine without blocking current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is cancelled.
async- Creates new coroutine and returns its future result as an implementation of Deferred. The running coroutine is canceled when the resulting object is cancelled.

Take a look at this piece of code as an example.

launch {
  delay(2000)
  println("launch block")
}

val result: Deferred<String> = async {
  delay(3000)
  "async block"
}

println(result.await())

From the example, the difference between launch and async is that async can return the future result which has a type of Deferred<T>, and we can call await() function to the Deferred variable to get the result of the Coroutine while launch only executes the code in the block without returning the result.

Coroutine Scope

Coroutine Scope defines a scope for coroutines. Every coroutine builder (like launch, async, etc) is an extension on CoroutineScope. When the scope dies, the Coroutines inside will be out of the picture too. Fortunately, Android lifecycle-viewmodel-ktx provides a really easy way to get a Coroutine Scope in the ViewModel. I will show you how to do so later.

Let’s start!

Dependencies

Here are the things you need to add to your build.gradle

dependencies {
    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:$latestRetrofitVersion"
    implementation "com.squareup.retrofit2:converter-gson:$latestRetrofitVersion"

    // OKHttp
    implementation "com.squareup.okhttp3:okhttp:$okHttpVersion"
    implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion"
    implementation "com.squareup.okhttp3:okhttp-urlconnection:$okHttpVersion"


    // Kotlin & Coroutines
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"

    // For Viewmodel to work with Coroutine
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleViewmodelKtxVersion"

}


API Interface

Let's create an API interface that defines the API path we going to deal with.

Normally, when defining the path in API interface we’ll do something like this right?

interface SomeApi {    
    @GET("path1/")  
    fun getObject1(): Call<Object1>
    
    @GET("path2/")   
    fun getObject2(): Call<Object2>   

    @GET("path3/{id}")   
    fun getObject3(@Path("id") objectId : String): Call<Object3> 
}

However, to make it work with Coroutine, we may do it this way.

interface SomeApi {    
 
   @GET("path1/")   
   suspend fun getObject1(): Response<Object1>

   @GET("path2/")
   suspend fun getObject2(): Response<Object2>    
  
   @GET("path3/{id}")   
   suspend fun getObject3(@Path("id") objectId : String): Response<Object3> 
}

Basically, we made our API interface’s functions as suspend function so that we can use them asynchronously later on.

Providing Retrofit API

object Api {
    fun getApiService(): SomeApi {
        val gson = GsonBuilder().create()
        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .client(provideOkHttpClient(provideLoggingInterceptor()))
            .addConverterFactory(GsonConverterFactory.create(gson))
            .build().create(SomeApi::class.java)
    }

    private fun provideOkHttpClient(interceptor: HttpLoggingInterceptor): OkHttpClient {
        val b = OkHttpClient.Builder()
        b.addInterceptor(interceptor)
        return b.build()
    }

    private fun provideLoggingInterceptor(): HttpLoggingInterceptor {
        val interceptor = HttpLoggingInterceptor()
        interceptor.level = HttpLoggingInterceptor.Level.BODY
        return interceptor
    }
}


Providing  Repository

class SomeRepository(val apiService: SomeApi) {

    suspend fun getObject1(): Response<Object1> = apiService.getObject1()

    suspend fun getObject2(): Response<Object2> = apiService.getObject2()

    suspend fun getObject3(objectId : String): Response<Object3> = apiService.getObject3((objectId)

}


Chaining network requests

Let’s say we have to create two network requests in a row and we have to use the result from the first request to create the second one.

In our example, suppose the path of endpoint path3/{id} depends on the result of the endpoint path1/

The code may look like this.

class SomeViewModel(
    private val repository: SomeRepository
) : ViewModel() {
    fun chainingRequest() {
        viewModelScope.launch {
            val object1 = repository.getObject1().body()
            val object3 = repository.getObject3(object1.id).body()
            processObject3(object3)
        }
    }
}

Create parallel network requests

Suppose we have to create two requests at the same time and process the results when the responses of both endpoints have returned. In our case, let’s call the endpoint /path1 and /path2 parallelly. The code going to look like this.


class SomeViewModel(
    private val repository: SomeRepository
) : ViewModel() {

    fun parallelRequest() {
        viewModelScope.launch {
            try {
                val getObject1Task = async { apiService.getObject1() }
                val getObject2Task = async { apiService.getObject2() }

                processData(getObject1Task.await().body(), getObject2Task.await().body())
            } catch (exception: Exception) {
                Log.e("TAG", exception.message)
            }
        }
    }
}


Let’s break it down.
  • The requests happen at val getObject1Task = async { apiService.getObject1() } and        val getObject2Task = async { apiService.getObject2() }. Although the code is written in the sequential, the requests perform asynchronously.
  • Then we pass the results of both requests to be process in successHandler. The results are obtained by getObject1Task.await().body() and getObject2Task.await().body()
  • The processData will only be called only when both getObject1Task and getObject2Task are finished.


Conclusion


Overall, I’m impressed by how coroutine helps us simplify asynchronous programming. Moreover, It gets us out of the callback hell and makes it easier to handle complex network requests. I think there is no excuse not to give coroutine a try now.

Source


Like 157 likes
Ben Kitpitak
Mobile Developer at OOZOU in Bangkok, Thailand
Share:

Join the conversation

This will be shown public
All comments are moderated

Get our stories delivered

From us to your inbox weekly.