In this blog, we will learn how to use retrofit with coroutines Kotlin in android application, with the help of an example. So we are actually building an application in android using Coroutines, Retrofit and MVVM, and so on. So lets started.
Objectives
Basically, the purpose of the project is to get the list of users information from the endpoints and display it to users in RecyclerView. I’m listing them below of these are objectives of this Retrofit coroutines example blog.
- Get a list of user’s info from an endpoint using REST API. Basically
- Use coroutines with Retrofit and MVVM
- Display the user’s info in a RecyclerView
So the main idea is to show you, how you can build a professional application using a proper architecture with Retrofit and using coroutines to get some information from a webserver. so that is what we are building.
Requirements
Now, there are some requirements for this retrofit coroutines example app. These are listed below
- Android knowledge
- Retrofit
- MVVM
So idea is, I’m not going to details of these, you have to basic idea of these items. You should know how retrofit works with MVVM. I’m going to focus, this example on coroutines that work with Retrofit and MVVM.
All right guys, so lets started quickly with this example app. Open AndroidStudio and create a new android project with some template.
Setting up the project
Let wait for sync of project, that recently you created. And first of all let’s add internet permission in AndroidManifest that is obvious, Because we are communicating with backend APIs. So we do need that
<uses-permission android:name="android.permission.INTERNET"/>
All right then next step is we have to add below dependency in app build.gradle.
- RecyclerView
- Glide
- Retrofit
- Lifecycle extensions
- Coroutines
// RecyclerView dependency
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
// Glide dependency
implementation 'com.github.bumptech.glide:glide:4.11.0'
// Retrofit dependency
implementation 'com.squareup.retrofit2:retrofit:2.7.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
// Lifecycle dependency
implementation 'android.arch.lifecycle:extensions:1.1.1'
// Coroutines dependency
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
Backend API Endpoint and JSON
So first of all, we require a REST API, that we have to consume in this project. You can use whatever you required. For now, I’m consuming this URL and json response is
{
"page": 1,
"per_page": 6,
"total": 12,
"total_pages": 2,
"data": [
{
"id": 1,
"email": "george.bluth@reqres.in",
"first_name": "George",
"last_name": "Bluth",
"avatar": "https://reqres.in/img/faces/1-image.jpg"
},
{
"id": 2,
"email": "janet.weaver@reqres.in",
"first_name": "Janet",
"last_name": "Weaver",
"avatar": "https://reqres.in/img/faces/2-image.jpg"
},
{
"id": 3,
"email": "emma.wong@reqres.in",
"first_name": "Emma",
"last_name": "Wong",
"avatar": "https://reqres.in/img/faces/3-image.jpg"
}
.
.
.
]
}
Create user Model
Now, we are following the MVVM design pattern in this coroutines example. So I’m creating a data class for parsing above JSON resonse. Let create a data class named is UserList.kt
package com.retrofitcoroutines.example.model
import com.google.gson.annotations.SerializedName
data class UserList(
@SerializedName("page") val page: Int,
@SerializedName("per_page") val per_page: Int,
@SerializedName("total") val total: Int,
@SerializedName("total_pages") val total_pages: Int,
@SerializedName("data") val data: List<User>
)
Let’s create another data class named User.kt
package com.retrofitcoroutines.example.model
import com.google.gson.annotations.SerializedName
data class User(
@SerializedName("id") val id : Int,
@SerializedName("email") val email : String,
@SerializedName("first_name") val first_name : String,
@SerializedName("last_name") val last_name : String,
@SerializedName("avatar") val avatar : String
)
Update the activity_main.xml
Now in main_activity has below layout, So we have a RecyclerView, then TextView in case of error, we can display a message. And we have a ProgressBar, so user know that is something is going in background. So its quite simple you know. So there are 3 element and we’re going to hide to show these element.
<?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=".view.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/usersList"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/listError"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:gravity="center"
tools:text="Error"
android:textColor="#FA1302"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/loadingView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Create a ViewModel
Let create a ViewModel named is ListViewModel. Here we simply fetching user list from backend server and passes that data to our MutableLiveData. Apart from this, we have a load error which generates an error message in case of API failure. and we have a loading MutableLiveData(), That let us know there is something happens in background.
package com.retrofitcoroutines.example.viewmodel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.retrofitcoroutines.example.model.User
import com.retrofitcoroutines.example.remote.UserService
import kotlinx.coroutines.*
class ListViewModel : ViewModel() {
val userService = UserService().getUsersService()
var job: Job? = null
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
val users = MutableLiveData<List<User>>()
val usersLoadError = MutableLiveData<String?>()
val loading = MutableLiveData<Boolean>()
fun refresh() {
fetchUsers()
}
private fun fetchUsers() {
loading.value = true
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = userService.getUsers()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
users.value = response.body()?.data
usersLoadError.value = null
loading.value = false
} else {
onError("Error : ${response.message()} ")
}
}
}
usersLoadError.value = ""
loading.value = false
}
private fun onError(message: String) {
usersLoadError.value = message
loading.value = false
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
Connect ListViewModel to MainActivity
In the MainActivity we are instantiating the ListViewModel. Then we simply set up our RecycleView, Using a UserAdapter, That we’ll create below of the code, and then we are observing the ViewModel, which simply connecting Livedata from the ViewModel and proceeding to update the interface based on the information that we received. So all is that we need to do.
package com.retrofitcoroutines.example.view
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import com.retrofitcoroutines.example.R
import com.retrofitcoroutines.example.viewmodel.ListViewModel
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
lateinit var viewModel: ListViewModel
private val userListAdapter = UserListAdapter(arrayListOf())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java)
viewModel.refresh()
usersList.apply {
layoutManager = LinearLayoutManager(context)
adapter = userListAdapter
}
observeViewModel()
}
private fun observeViewModel() {
viewModel.users.observe(this, Observer {countries ->
countries?.let {
usersList.visibility = View.VISIBLE
userListAdapter.updateCountries(it) }
})
viewModel.usersLoadError.observe(this, Observer { isError ->
listError.visibility = if(isError == "") View.GONE else View.VISIBLE
})
viewModel.loading.observe(this, Observer { isLoading ->
isLoading?.let {
loadingView.visibility = if(it) View.VISIBLE else View.GONE
if(it) {
listError.visibility = View.GONE
usersList.visibility = View.GONE
}
}
})
}
}
Now create UserListAdapter
Simply create a RecyclerView Adapter and attached the user_item view to recycler as usual. The only different thing here is the extension function for ImageView to load the image which allows us to loan an image on ImageView based on the URL we have.
package com.retrofitcoroutines.example.view
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.retrofitcoroutines.example.R
import com.retrofitcoroutines.example.model.User
import com.retrofitcoroutines.example.utils.loadImage
import kotlinx.android.synthetic.main.item_user.view.*
class UserListAdapter(var users: ArrayList<User>): RecyclerView.Adapter<UserListAdapter.UserViewHolder>() {
fun updateCountries(newUsers: List<User>) {
users.clear()
users.addAll(newUsers)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, p1: Int) = UserViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false)
)
override fun getItemCount() = users.size
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(users[position])
}
class UserViewHolder(view: View): RecyclerView.ViewHolder(view) {
private val imageView = view.userAvatar
private val userName = view.userFullName
private val userEmail = view.userEmail
fun bind(user: User) {
userName.text = user.first_name + " "+ user.last_name
userEmail.text = user.email
imageView.loadImage(user.avatar)
}
}
}
Here is item_user.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="wrap_content"
android:layout_margin="8dp"
android:background="#E4DDF3">
<ImageView
android:id="@+id/userAvatar"
android:layout_width="80dp"
android:layout_height="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@mipmap/ic_launcher_round" />
<TextView
android:id="@+id/userFullName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:textColor="@color/colorPrimaryDark"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/userAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="Morris" />
<TextView
android:id="@+id/userEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
tools:text="morris@androidwave.com"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/userAvatar"
app:layout_constraintTop_toBottomOf="@+id/userFullName" />
</androidx.constraintlayout.widget.ConstraintLayout>
Write a extension function for ImageView
Here I have created a new package named is utils and writes below extension function.
package com.retrofitcoroutines.example.utils
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.retrofitcoroutines.example.R
fun ImageView.loadImage(uri: String?) {
val options = RequestOptions()
.error(R.mipmap.ic_launcher_round)
Glide.with(this.context)
.setDefaultRequestOptions(options)
.load(uri)
.into(this)
}
That all about UI let setup Retrofit and connect it with project.
Setting up Retrofit
Let create a retrofit interface. Here I’m gonna set up a GET function for Retrofit. Inside this interface create a suspending function like below. It simply returns us a Response.
package com.retrofitcoroutines.example.remote
import com.retrofitcoroutines.example.model.UserList
import retrofit2.Response
import retrofit2.http.GET
interface UserApi {
@GET("users")
suspend fun getUsers(): Response<UserList>
}
In this class, first of I have created a variable here is BASE_URL. After that I have created a function named is getUsersService() that simply returns UserApi that we just defined earlier.
package com.retrofitcoroutines.example.remote
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class UserService {
private val BASE_URL = "https://reqres.in/api/";
fun getUsersService(): UserApi{
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(UserApi::class.java)
}
}
That all done, let’s go ahead and run the code and see if it works. It’s working so we have all users that return from APIs in the context of coroutines. Basically, in this example, we have two coroutines because first, we are calling with Dispatchers.IO and finally we are calling Dispatchers.Main. So way we can update UI in main. thread. That code I already added in ViewModel, for reference I adding again
CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val response = userService.getUsers()
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
users.value = response.body()?.data
usersLoadError.value = null
loading.value = false
} else {
onError("Error : ${response.message()} ")
}
}
}
Conclusion
So that is it, So we learned how to use Retrofit with coroutines in Kotlin| Android. I hope it’s helpful for you, Help me by sharing this post with all your friends who learning android app development.
Download Source Code
Retrofit coroutines error handling will publish a complete article on that, For now you can follow this article
Retrofit with coroutines This is a sample app thats created. Please follow this article only