Kotlin Coroutines with Retrofit

Upasana | August 01, 2020 | 4 min read | 2,667 views


In this tutorial we will use android architecture components for making Kotlin coroutine enabled REST API calls using Retrofit in Android Application.

Gradle setup

We will be adding the following dependencies to our build.gradle - android architecture components, kotlin extensions for activity and fragments, and retrofit + okhttp

build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    //Android architecture components - Kotlin version
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc03'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc03'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc03'

    //Kotlin extensions for activity and fragments
    implementation 'androidx.fragment:fragment-ktx:1.1.0'
    implementation "androidx.activity:activity-ktx:1.0.0"

    //Retrofit and OkHttp dependencies
    implementation 'com.squareup.okhttp3:okhttp:4.2.2'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
    implementation 'com.squareup.retrofit2:retrofit:2.6.2'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
    implementation 'com.squareup.retrofit2:converter-scalars:2.6.2'
    implementation 'com.google.code.gson:gson:2.8.6'
}

Creating Retrofit API Client

First of all we need to create an instance of OkHttpClient and Retrofit client that we will use later on for creating instance of Services.

ApiClient.kt - retrofit client singleton
object ApiClient {

    private val okHttpClient by lazy { OkHttpClient() }

    private val retrofit: Retrofit by lazy {
        Log.e("AppClient", "Creating Retrofit Client")
        val builder = Retrofit.Builder()
            .baseUrl("https://reqres.in")
            .addConverterFactory(ScalarsConverterFactory.create())
            .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create()))
        val dispatcher = Dispatcher()
        dispatcher.maxRequests = 1

        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY

        val client: OkHttpClient = okHttpClient.newBuilder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .addInterceptor(loggingInterceptor)
            .dispatcher(dispatcher)
            .build()
        builder.client(client).build()
    }

    fun <T> createService(tClass: Class<T>?): T {
        return retrofit.create(tClass)
    }
}

Creating Service Interface

For this tutorial, we will be using a sample hosted service at https://reqres.in that returns person details.

EmployeeApi.kt
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
import retrofit2.Response
import retrofit2.http.GET

/**
 * @author Munish Chandel
 */

interface EmployeeApi {

    @GET("/api/users/2")
    suspend fun getPerson(): Response<ServiceResponse<Person>>  (1)
}

@Keep
data class ServiceResponse<T>(
    @SerializedName("data") val data: T
)

@Keep
data class Person(
    @SerializedName("id") val id: Long,
    @SerializedName("email") val email: String,
    @SerializedName("first_name") val firstName: String,
    @SerializedName("last_name") val lastName: String
)
1 suspend function allows us to run this method inside a coroutine. suspend variant is only available since retrofit 2.6+

Generic data class for holding network responses

We need a class that can hold data as well as network status like loading, success and error. The following implementation shall be good enough to start with.

Resource.kt
data class Resource<out T>(val status: Status,
                           val data: T?,
                           val msg: String?) {
    companion object {
        fun <T> success(data: T?): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String, data: T? = null): Resource<T> {
            return Resource(Status.ERROR, data, msg)
        }

        fun <T> loading(data: T? = null): Resource<T> {
            return Resource(Status.LOADING, data, null)
        }
    }
}

enum class Status {
    SUCCESS,
    ERROR,
    LOADING
}

Creating ViewModel for interacting with Service

ViewModel will interact with the service and fetches the data from network using Kotlin coroutine tied to viewModelScope. So the request will be automatically cancelled if fragment is destroyed.

MainViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import kotlinx.coroutines.Dispatchers

class MainViewModel : ViewModel() {

    fun loadData() = liveData(Dispatchers.IO) { (1)
        emit(Resource.loading())
        val api = ApiClient.createService(EmployeeApi::class.java)
        val response = api.getPerson()
        if(response.isSuccessful) {
            emit(Resource.success(response.body()?.data))
        }
    }
}
1 liveData is handy kotlin extension function that enables us to create and run a Kotlin coroutine tied to viewModelScope.

Fragment using ViewModel for communication

MainFragment.kt
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.shunya.myapplication.R
import kotlinx.android.synthetic.main.main_fragment.*

class MainFragment : Fragment() {

    companion object {
        fun newInstance() = MainFragment()
    }

    private val viewModel: MainViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        return inflater.inflate(R.layout.main_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel.loadData().observe(this, Observer { networkResource ->
            when (networkResource.status) {
                Status.LOADING -> {
                    message.text = "loading data from network"
                }
                Status.SUCCESS -> {
                    val person = networkResource.data
                    person?.let {
                        message.text =
                            person.firstName + " " + person.lastName + "\n" + person.email
                    }
                }
                Status.ERROR -> {
                    message.text = "error loading data from network"
                }
            }
        })
    }

}

references


Top articles in this category:
  1. Retrofit Basic Authentication in Android
  2. Retrofit OAuth2 Bearer Token Authentication OkHttp Android
  3. Service vs Intent Service in Android
  4. Firebase Cloud Messaging in Android App using Command Pattern
  5. iOS interview experience fresher
  6. iOS interview questions for 0-3 years experience
  7. FirebaseInstanceIdService is deprecated now

Recommended books for interview preparation:

Find more on this topic:
Buy interview books

Java & Microservices interview refresher for experienced developers.