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> }
Create a UsersService
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. I would to request read modern way of handling background jobs using coroutines .
What is recommendation
If you are a beginner I would like to recommend please read basic concept of coroutines.
- Coroutines for beginners
- Coroutine Scope in Kotlin
- Suspending functions in Coroutine
- Dispatchers in Kotlin Coroutines
- Exception handling in coroutines
2 Comments
Thanks for writing this… but the code shown for the “Connect ListViewModel to MainActivity” section just repeats the code from the ListViewModel, and to download the source, I am forced to submit my email address. Not a fan of that…
Sorry, Jeremy, I update this repeated block, Thank’s