Using Retrofit with Kotlin Coroutines in Android

Networking is one of the most important parts of Android Applications. One of the most popular libraries used for Networking in Android is Retrofit. The main reason Retrofit is popular among Android Networking libraries is that it reduces a lot of Boilerplate code and helps in consuming the web service easily. Also, it keeps updating with the latest trends such as compatibility with Rxjava and now, The Coroutine Support!

Welcome to our MindOrks blog on Using Retrofit with Koltin Coroutines in Android!

Starting from version 2.6.0 Retrofit supports the concept of “suspend” functions.

Before proceeding further, we would recommend our MindOrks blog for better understanding of Coroutines

In this blog we are going to understand with a sample project:

  • How to use retrofit 2.6.0 with coroutines?
  • What is the suspend keyword?
  • What is the LiveData scope in Coroutines?

We are going to understand these concepts with a project. Before discussing this topic, if you need to refresh the basics of MVVM project, please refer to our MVVM Architecture-Beginner’s Tutorial blog.

We strongly recommend you to go through the example project provided in MVVM Architecture-Beginner’s Tutorial blog as we are going to improvise on the same in this blog.

Set up a new project with Kotlin and other dependencies required

Here, we are going to set up the Android Project.

Create a Project

  • Start a new Android Studio Project
  • Select Empty Activity and Next
  • Name: Retrofit-Kotlin-Coroutines-Example
  • Package name: com.mindorks.retrofit.coroutines
  • Language: Kotlin
  • Finish
  • Your starting project is ready now

Add dependencies

Add the following dependencies in your app level build.gradle.

implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.github.bumptech.glide:glide:4.9.0'

//LifeCycle
implementation 'androidx.lifecycle:lifecycle-common:2.2.0'
implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0'
implementation 'android.arch.lifecycle:extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0'

//Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'

Here, we can see that we have added our Retrofit and Coroutines dependencies. Since we will be launching the coroutines in the live data scope(we will be discussing this later in this blog), we add the required life cycle dependencies.

Project Structure

For the project, we are going to follow a beginner version of MVVM. Our package in the project will look like below, similar to our MVVM beginner’s tutorial blog.

Setting up the Utils

We set up the utils package exactly the way we set it up in our MVVM beginner’s tutorial blog.

Firstly, the Enum status class:

package com.mindorks.retrofit.coroutines.utils

enum class Status {
    SUCCESS,
    ERROR,
    LOADING
}

Now, the Resource class:

package com.mindorks.retrofit.coroutines.utils

import com.mindorks.retrofit.coroutines.utils.Status.ERROR
import com.mindorks.retrofit.coroutines.utils.Status.LOADING
import com.mindorks.retrofit.coroutines.utils.Status.SUCCESS


data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
    companion object {
        fun <T> success(data: T): Resource<T> = Resource(status = SUCCESS, data = data, message = null)

        fun <T> error(data: T?, message: String): Resource<T> =
            Resource(status = ERROR, data = data, message = message)

        fun <T> loading(data: T?): Resource<T> = Resource(status = LOADING, data = data, message = null)
    }
}

Setting up the Model

Since we will be using the same API service we have used in our MVVM beginner’s tutorial blog, we will be creating a similar model class.

Server Response:

{
    "id": "Id of the User",
    "name": "Name of the User",
    "avatar": "url of the User avatar",
    "email": "Email of the user"
}

Model class:

package com.mindorks.retrofit.coroutines.data.model

data class User(
    val avatar: String,
    val email: String,
    val id: String,
    val name: String
)

Note: Here we haven’t used the @SerializedName annotation since we have declared the same variable name as that of the server response field. However, if you wish to change the variable name of the data class, you can do so by adding the annotation like follows.

package com.mindorks.retrofit.coroutines.data.model

import com.google.gson.annotations.SerializedName

data class User(
    @SerializedName("avatar")
    val image: String,
    @SerializedName("email")
    val userEmail: String,
    @SerializedName("id")
    val userId: String,
    @SerializedName("name")
    val userName: String
)

Setting up the Network Layer

Now, since we are using Retrofit for Network calls, let’s create a class that provides us the instance of the Retrofit Service class.

Note: We are going to come across some new keywords such as “suspend” in this Network layer. We are going to understand this later in this blog. First, let’s set up the project.

Retrofit Service class

package com.mindorks.retrofit.coroutines.data.api

import com.mindorks.retrofit.coroutines.data.model.User
import retrofit2.http.GET

interface ApiService {

    @GET("users")
    suspend fun getUsers(): List<User>

}

Retrofit Builder class:

package com.mindorks.retrofit.coroutines.data.api

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitBuilder {

    private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"

    private fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build() //Doesn't require the adapter
    }

    val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}

Now, let’s create an API Helper class to help us with the ApiService call:

package com.mindorks.retrofit.coroutines.data.api

class ApiHelper(private val apiService: ApiService) {

    suspend fun getUsers() = apiService.getUsers()
}

Our Network Layer is now set.

Since we will be using a Repository pattern, we will be linking our ApiHelper class by using a Repository class:

package com.mindorks.retrofit.coroutines.data.repository

import com.mindorks.retrofit.coroutines.data.api.ApiHelper

class MainRepository(private val apiHelper: ApiHelper) {

    suspend fun getUsers() = apiHelper.getUsers()
}

Setting up the ViewModel

Note: We are going to come across some new keywords such as “Coroutines”, “liveData scope”, “Dispatchers” in this ViewModel. We are going to understand this later in this blog. First, let’s set up the project.

Now, that our model and network layers are set, let’s set up our ViewModel

package com.mindorks.retrofit.coroutines.ui.main.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.mindorks.retrofit.coroutines.data.repository.MainRepository
import com.mindorks.retrofit.coroutines.utils.Resource
import kotlinx.coroutines.Dispatchers


class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {

    fun getUsers() = liveData(Dispatchers.IO) {
        emit(Resource.loading(data = null))
        try {
            emit(Resource.success(data = mainRepository.getUsers()))
        } catch (exception: Exception) {
            emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
        }
    }
}

We will be providing our View Model from a Factory class. So let’s construct our ViewModelFactory class:

package com.mindorks.retrofit.coroutines.ui.base

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.mindorks.retrofit.coroutines.data.api.ApiHelper
import com.mindorks.retrofit.coroutines.data.repository.MainRepository
import com.mindorks.retrofit.coroutines.ui.main.viewmodel.MainViewModel

class ViewModelFactory(private val apiHelper: ApiHelper) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(MainRepository(apiHelper)) as T
        }
        throw IllegalArgumentException("Unknown class name")
    }

}

Setting up the View

Our MainActivity class:

package com.mindorks.retrofit.coroutines.ui.main.view

import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.mindorks.retrofit.coroutines.R.layout
import com.mindorks.retrofit.coroutines.data.api.ApiHelper
import com.mindorks.retrofit.coroutines.data.api.RetrofitBuilder
import com.mindorks.retrofit.coroutines.data.model.User
import com.mindorks.retrofit.coroutines.ui.base.ViewModelFactory
import com.mindorks.retrofit.coroutines.ui.main.adapter.MainAdapter
import com.mindorks.retrofit.coroutines.ui.main.viewmodel.MainViewModel
import com.mindorks.retrofit.coroutines.utils.Status.ERROR
import com.mindorks.retrofit.coroutines.utils.Status.LOADING
import com.mindorks.retrofit.coroutines.utils.Status.SUCCESS
import kotlinx.android.synthetic.main.activity_user.progressBar
import kotlinx.android.synthetic.main.activity_user.recyclerView

class MainActivity : AppCompatActivity() {

    private lateinit var viewModel: MainViewModel
    private lateinit var adapter: MainAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(layout.activity_main)
        setupViewModel()
        setupUI()
        setupObservers()
    }


    private fun setupViewModel() {
        viewModel = ViewModelProviders.of(
            this,
            ViewModelFactory(ApiHelper(RetrofitBuilder.apiService))
        ).get(MainViewModel::class.java)
    }

    private fun setupUI() {
        recyclerView.layoutManager = LinearLayoutManager(this)
        adapter = MainAdapter(arrayListOf())
        recyclerView.addItemDecoration(
            DividerItemDecoration(
                recyclerView.context,
                (recyclerView.layoutManager as LinearLayoutManager).orientation
            )
        )
        recyclerView.adapter = adapter
    }

    private fun setupObservers() {
        viewModel.getUsers().observe(this, Observer {
            it?.let { resource ->
                when (resource.status) {
                    SUCCESS -> {
                        recyclerView.visibility = View.VISIBLE
                        progressBar.visibility = View.GONE
                        resource.data?.let { users -> retrieveList(users) }
                    }
                    ERROR -> {
                        recyclerView.visibility = View.VISIBLE
                        progressBar.visibility = View.GONE
                        Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
                    }
                    LOADING -> {
                        progressBar.visibility = View.VISIBLE
                        recyclerView.visibility = View.GONE
                    }
                }
            }
        })
    }

    private fun retrieveList(users: List<User>) {
        adapter.apply {
            addUsers(users)
            notifyDataSetChanged()
        }
    }
}

Our activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.main.view.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

In order to load our Recycler view, we have to create a row item type:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/container"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="60dp">

    <ImageView
        android:id="@+id/imageViewAvatar"
        android:layout_width="60dp"
        android:layout_height="0dp"
        android:padding="4dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:ignore="contentDescription" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textViewUserName"
        style="@style/TextAppearance.AppCompat.Large"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="4dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="MindOrks" />

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/textViewUserEmail"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="@+id/textViewUserName"
        app:layout_constraintTop_toBottomOf="@+id/textViewUserName"
        tools:text="MindOrks" />

</androidx.constraintlayout.widget.ConstraintLayout>

Note: For the ease of understanding, the dimensions are hardcoded in the provided layout files. It is strongly recommended to read the dimensions and strings from their respective dimens.xml files and strings.xml files for best coding practices.

Finally, to load our RecyclerView, we need an adapter class with a view Holder:

package com.mindorks.retrofit.coroutines.ui.main.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mindorks.retrofit.coroutines.R
import com.mindorks.retrofit.coroutines.data.model.User
import com.mindorks.retrofit.coroutines.ui.main.adapter.MainAdapter.DataViewHolder
import kotlinx.android.synthetic.main.item_layout.view.imageViewAvatar
import kotlinx.android.synthetic.main.item_layout.view.textViewUserEmail
import kotlinx.android.synthetic.main.item_layout.view.textViewUserName

class MainAdapter(private val users: ArrayList<User>) : RecyclerView.Adapter<DataViewHolder>() {

    class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        fun bind(user: User) {
            itemView.apply {
                textViewUserName.text = user.name
                textViewUserEmail.text = user.email
                Glide.with(imageViewAvatar.context)
                    .load(user.avatar)
                    .into(imageViewAvatar)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DataViewHolder =
        DataViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false))

    override fun getItemCount(): Int = users.size

    override fun onBindViewHolder(holder: DataViewHolder, position: Int) {
        holder.bind(users[position])
    }

    fun addUsers(users: List<User>) {
        this.users.apply {
            clear()
            addAll(users)
        }

    }
}

That’s it, our project is set up. Let’s add our internet permission in the AndroidManifest file:

<uses-permission android:name="android.permission.INTERNET"/>

Now, let’s run our project. Awesome! We can see the server response on the screen!

Understanding Retrofit with Kotlin CoroutinesNow that we have worked on the project, let’s understand two key things:

Suspend

The suspend functions can only be called from Coroutines. Adding the keyword suspend helps the coroutine to suspend (pause), perform the required job on a network thread (if Dispatchers.IO) is used, wait for the response, and then resumes from where it left off once the response is available. In our example code, the ViewModel class:

fun getUsers() = liveData(Dispatchers.IO) {
        emit(Resource.loading(data = null))
        try {
            emit(Resource.success(data = mainRepository.getUsers()))
        } catch (exception: Exception) {
            emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
        }
    }

Here, the getUsers function of the MainRepository class is a suspend function, and hence, only once the network call(which is run on another thread, in this case, the thread from Dispatchers.IO) is completed (success or error), the coroutine resumes by emitting the respective value that is obtained from the network call.

LifeCycle Scope

A LifecycleScope is defined for each Lifecycle object. LifecycleOwner could be an Activity or a Fragment. Any coroutine launched in this scope is canceled when the Lifecycle is destroyed. This helps us in avoiding memory leaks.

Here we have used liveData(Dispatchers.IO). If we observe the import statement:

import androidx.lifecycle.liveData

Hence, the result of the function will be emitted as Live Data, which can be observed in the view (Activity or Fragment).

Project Source Code and What Next?

You can find the complete project here.

As we have done some simplifications in this project for the Beginners level, so, we can improve this project to go to the Advanced level, a few of the things which we can improve are as follows:

  • Implement Dependency Inject Framework - Dagger in the project.
  • Make ApiService Singleton and reuse the same instance for all the features.
  • Create base classes such as BaseActivity.
  • Handle all the API errors at a single place in a better way.
  • Create Interfaces for the classes wherever required.
  • Take advantage of Android KTX - Kotlin Extensions.
  • Write Unit-Test
  • and so on.

Want to learn advanced concepts in Android? Apply Now: MindOrks Android Online Course and Learn Advanced Android

That’s it for this article!

We hope that you have understood how to use Retrofit with coroutines in a simple way!

Thank you so much for your time!

Team MindOrks!

Also, Let’s connect on Twitter, Linkedin, and Github