How Using Retrofit with Kotlin Coroutines in Android (MVVM)

nagendran p
7 min readJul 20, 2020

--

What is Coroutines

Essentially, coroutines are light-weight threads. It lets us write asynchronous, non-blocking code in a sequential way.

What problems do coroutines solve?

On Android, coroutines are a great solution to two problems:

  1. Long running tasks are tasks that take too long to block the main thread.
  2. Main-safety allows you to ensure that any suspend function can be called from the main thread.

RxJava VS Coroutines

  1. Coroutines is more efficient than RxJava as it uses less resources to execute the same task while also being faster in doing so. RxJava uses greater amounts of memory and requires more CPU time, which translates into higher battery consumption and possible UI interruptions for the user.
  2. RxJava is a pretty heavy library with quite large variety of ready-to-use operators.
  3. If the app needs to be simple and easily read by less experienced coders, use Coroutines
  4. If the app needs high levels of data manipulation between fetching and emitting, use RxJava

Role of Suspend and Resume

Coroutines build upon regular functions by adding two new operations. In addition to invoke (or call) and return, coroutines add suspend and resume.

  • suspend — pause the execution of the current coroutine, saving all local variables
  • resume — continue a suspended coroutine from the place it was paused

This functionality is added by Kotlin by the suspend keyword on the function. You can only call suspend functions from other suspend functions, or by using a coroutine builder like launch to start a new coroutine.

Suspend and resume work together to replace callbacks.

Main-safety with coroutines

Using suspend doesn’t tell Kotlin to run a function on a background thread.

Using Dispatchers for which thread runs the coroutine

+ — — — — — — — — — — — — — — — — — -+
| Dispatchers.Main |
+ — — — — — — — — — — — — — — — — — -+
| Main thread on Android, interact |
| with the UI and perform light |
| work |
+ — — — — — — — — — — — — — — — — — -+
| — Calling suspend functions |
| — Call UI functions |
| — Updating LiveData |
+ — — — — — — — — — — — — — — — — — -+

+ — — — — — — — — — — — — — — — — — -+
| Dispatchers.IO |
+ — — — — — — — — — — — — — — — — — -+
| Optimized for disk and network IO |
| off the main thread |
+ — — — — — — — — — — — — — — — — — -+
| — Database* |
| — Reading/writing files |
| — Networking** |
+ — — — — — — — — — — — — — — — — — -+

+ — — — — — — — — — — — — — — — — — -+
| Dispatchers.Default |
+ — — — — — — — — — — — — — — — — — -+
| Optimized for CPU intensive work |
| off the main thread |
+ — — — — — — — — — — — — — — — — — -+
| — Sorting a list |
| — Parsing JSON |
| — DiffUtils |
+ — — — — — — — — — — — — — — — — — -+

References for before get start the Project

  • Official Kotlin Coroutines docs
    It’s good to read the official docs before using it.
  • Official Kotlin Coroutines Codelabs
  • A great medium series introducing Kotlin Coroutines:
  • Team lead of Kotlin libraries talks about the blocking threads and suspending coroutines.

Add dependencies

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

implementation 'com.google.android.material:material:1.1.0'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'
android{
dataBinding{
enabled=true;
}
}

Utils

Firstly, the Enum status class:

package com.example.mycoroutinessample.util

enum class Status {
SUCCESS,
ERROR,
LOADING
}

Now, the Resource class:

package com.example.mycoroutinessample.util

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= Status.SUCCESS,data = data,message = null)
fun<T> error(data:T?,message: String?):Resource<T> = Resource(status= Status.ERROR,data = data,message = message) fun<T> loading(data:T?):Resource<T> = Resource(status= Status.LOADING,data = data,message = null)
}

Model

@Parcelize
data class User(
@Expose
@SerializedName("id")
var id: String,
@Expose
@SerializedName("name")
var name: String,
@Expose
@SerializedName("title")
var title: String,
@Expose
@SerializedName("message")
var message: String
) : Parcelable

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

import com.example.mycoroutinessample.data.model.User
import com.google.gson.JsonObject
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.POST
import java.util.*

interface ApiService {


@FormUrlEncoded
@POST("/posts/insert.php")
suspend fun createUser(@FieldMap body: HashMap<String, String>): User

@GET("/posts/show.php")
suspend fun getUsers(): List<User>

@FormUrlEncoded
@POST("/posts/update.php")
suspend fun updateUser(@FieldMap body: HashMap<String, String>): User

// Delete note
@FormUrlEncoded
@POST("/posts/delet.php")
suspend fun deleteUser(@FieldMap body: HashMap<String, String>): JsonObject

}

Retrofit Builder class:

import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object ApiClient {
//please use your own url var BASE_URL:String="http://localhost/posts/"

fun apiClient(): Retrofit{

val gson = GsonBuilder()
.setLenient()
.create()

val interceptor = HttpLoggingInterceptor()
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)

val client = OkHttpClient.Builder().addInterceptor(interceptor).build()

val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()

return retrofit

}

}

ViewModel

Note: We are going to come across some new keywords such =“liveData scope” in this ViewModel.

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).

package com.example.mycoroutinessample.ui.main.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.example.mycoroutinessample.util.Resource
import com.example.recyclerviewex.ApiService
import kotlinx.coroutines.Dispatchers
import java.lang.Exception
import java.util.HashMap

class MainViewModel(private val apiService: ApiService) : ViewModel() {

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

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

package com.example.mycoroutinessample.ui.base

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import
com.example.mycoroutinessample.ui.main.viewmodel.MainViewModel
import com.example.recyclerviewex.ApiService
import java.lang.IllegalArgumentException

class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory{
override fun <T : ViewModel?> create(modelClass: Class<T>): T {

if(modelClass.isAssignableFrom(MainViewModel::class.java)){
return MainViewModel(apiService)as T
}

throw IllegalArgumentException("Unknown class name")
}
}

View

MainActivity.class

package com.example.mycoroutinessample.ui.main.view

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.mycoroutinessample.R
import com.example.mycoroutinessample.data.model.User
import com.example.mycoroutinessample.databinding.ActivityMainBinding
import com.example.mycoroutinessample.ui.base.ViewModelFactory
import com.example.mycoroutinessample.ui.main.adapter.UserListAdapter
import com.example.mycoroutinessample.ui.main.viewmodel.MainViewModel
import com.example.mycoroutinessample.util.Status
import com.example.recyclerviewex.ApiClient
import com.example.recyclerviewex.ApiService
import com.google.gson.JsonObject

class MainActivity : AppCompatActivity() {

lateinit var mainBinding: ActivityMainBinding

var userList = ArrayList<User>()


private lateinit var viewModel: MainViewModel
private lateinit var adapter: UserListAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)

mainBinding.mainActivity = this

setupViewModel()

setupUI()

setUpObserver()

}

private fun setupViewModel() {
viewModel = ViewModelProviders.of(
this,
ViewModelFactory(ApiClient.apiClient().create(ApiService::class.java))
).get(MainViewModel::class.java)

}
private fun setupUI() {

mainBinding.recyclerView.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL,false)

mainBinding.recyclerView.setItemAnimator(DefaultItemAnimator())

adapter = UserListAdapter(userList, object : UserListAdapter.ClicListener {

override fun update(position: Int) {
}

override fun delete(position: Int) {
}
})
mainBinding.recyclerView.adapter = adapter
}


private fun setUpObserver() {

viewModel.getUsers().observe(this, Observer {

it
?.let { resource ->
when
(resource.status) {

Status.SUCCESS -> {
showProgress(false)
resource.data?.let { users ->retrieveList(users) }
}
Status.ERROR -> {
showProgress(false)
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
Status.LOADING -> {
showProgress(true)
}
}

}
}
)
}

private fun retrieveList(users: List<User>) {
if (users.size > 0) {
userList.addAll(users)
adapter.notifyDataSetChanged()
}
}


private fun showProgress(status: Boolean) {
if (status) {
mainBinding.showProgress.visibility = View.VISIBLE
} else {
mainBinding.showProgress.visibility = View.GONE
}
}
}

our activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<layout
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"
>


<data>
<import type="android.view.View" />

<variable
name="mainActivity"
type="com.example.mycoroutinessample.ui.main.view.MainActivity"
/>

<variable
name="userViewModel"
type="com.example.mycoroutinessample.ui.main.viewmodel.MainViewModel"
/>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".view.MainActivity"
>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!"
/>

</LinearLayout>

<ProgressBar
android:id="@+id/show_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
/>


</FrameLayout>

</layout>

UserListAdapter.java

package com.example.mycoroutinessample.ui.main.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.mycoroutinessample.R
import com.example.mycoroutinessample.data.model.User

class UserListAdapter(val userList: ArrayList<User>, val mListener: ClicListener) : RecyclerView.Adapter<UserListAdapter.ViewHolder>() {


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_layout, parent, false)
return ViewHolder(v)
}

override fun getItemCount(): Int {
return userList.size
}

override fun onBindViewHolder(holder: ViewHolder, position: Int){
holder.bindItems(userList.get(position))
}

interface ClicListener {
fun update(position: Int)
fun delete(position: Int)

}

inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),View.OnClickListener {


fun bindItems(user: User) {

val name = itemView.findViewById<TextView>(R.id.name)
val title = itemView.findViewById<TextView>(R.id.title)
val message = itemView.findViewById<TextView>(R.id.message)

val update = itemView.findViewById<ImageView>(R.id.update)
val delete = itemView.findViewById<ImageView>(R.id.delete)

name.text = user.name
title.text = user.title
message.text = user.message

update.setOnClickListener(this)
delete.setOnClickListener(this)
}

override fun onClick(view: View?) {

if (view?.id == R.id.update)
mListener.update(adapterPosition)
if (view?.id == R.id.delete)
mListener.delete(adapterPosition)

}

}
}

list_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp"
>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="15sp"
android:layout_gravity="center"
android:gravity="left"
android:textStyle="bold"
/>

<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
>

<ImageView
android:id="@+id/update"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/updateicon"
android:padding="5dp"
android:layout_margin="5dp"
android:layout_gravity="center"
android:layout_weight="1"
/>

<ImageView
android:id="@+id/delete"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/deleteicon"
android:padding="5dp"
android:layout_margin="5dp"
android:layout_gravity="center"
android:layout_weight="1"
/>


</LinearLayout>

</LinearLayout>


<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:gravity="left"
android:padding="5dp"
/>
<TextView
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="13sp"
android:gravity="left"
android:padding="5dp"
/>

<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000"
/>

</LinearLayout>

My Full Project Source Code here

Happy Coding

--

--