Let’s learn how to implement MVVM in Android.
It’s actually really simple.
ViewModel is just another class that that implements the main Business Logic.
ViewModel is a middle man between your view and your Data class(Repo/any Data Provider).
It can be represented simply like this.
Here the View just displays the data.
ViewModel doesn’t know where the data is coming from which makes it reusable.
Repo takes care of the data, it can be from the Server, Local DB or files or somewhere else.
Here is the Key
Since we are using ViewModel and LiveData which are Activity lifecycle aware, it will lead to fewer crashes and memory leaks.
Aim
Here our aim is to fetch a list of users from a webservice and show it in a ListView.
Below is the webservice we are going to consume.
https://jsonplaceholder.typicode.com/users
#1. Create Model classes for the Web-Service response.
You can simply grap the response from the websservice and paste it in the below website to generate Java Models.
https://www.jsonschema2pojo.org/
Once you have all the java moodels, it is easy to convert it to Kotlin code.
Simply create corresponding Kotlin classes and paste the java code onto it.
Android Studio will automatically convert all the java code to Kotlin.
As simple as that.
The Model classes for this demo is available here if you want to grab it quickly.
Model classes for above webservice
Okay now we have the model classes.
In addition to the above classes, we also have an ErrorModel class which is used for the errors in the Service.
data class UserError( val code: Int, val message: String )
#2. Create the Repo Class.
Our Repo class will use Retrofit to call the webservice and parse it using gson.
Add the below dependencies in your project build.gradle and sync the project.
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
Now let’s create the following classes.
1. RetrofitUtil.kt // Base Retrofit class
2. UserRepo.kt. // Repo class for User Service
3. Constants.kt. // Class holding the app constants
4. UserInterface.kt // Retrofit User interface class
5. UsersCallback.kt // interface to get the callback from the repo to the ViewModel.
Constants
This calls holds our constants.
package com.coderzheaven.tutorialprojects.constants class Constants { companion object { const val BASE_URL = "https://jsonplaceholder.typicode.com/" const val USERS = "users" // Errors const val UNKNOWN_ERROR = "Unknown Error" const val UNKNOWN_ERROR_CODE = 100 const val USER_LOAD_FAILURE = 101 } }
RetrofitUtil
This class will create the Retrofit builder object for the service calls.
package com.coderzheaven.tutorialprojects.repo import com.coderzheaven.tutorialprojects.constants.Constants.Companion.BASE_URL import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory class RetrofitUtil { companion object { private val client = OkHttpClient.Builder().build() private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .client(client) .build() fun <T> buildService(service: Class<T>): T { return retrofit.create(service) } } }
UserInterface
This class is for the UserWebService which is needed by Retrofit.
package com.coderzheaven.tutorialprojects.`interface` import com.coderzheaven.tutorialprojects.models.User import retrofit2.Call import retrofit2.http.GET interface UsersInterface { @GET(USERS) fun getUsers(): Call<List<User>> }
The Callback interface
UsersCallback
The callback class which is passsed to the repo constructor to get the callback to the viewmodel.
package com.coderzheaven.tutorialprojects.callback import com.coderzheaven.tutorialprojects.models.User import com.coderzheaven.tutorialprojects.models.UserError interface UsersCallback { fun onSuccess(users: List<User>) fun onFailed(userError: UserError) fun onLoading(loading: Boolean) }
UserRepo
Class which calls the webservice and get the data, then it returns the data back to the ViewModel with the help of the interface we send in the constructor.
Not to be confused with the “UsersInterface” which is associated with Retrofit.
package com.coderzheaven.tutorialprojects.repo import com.coderzheaven.tutorialprojects.`interface`.UsersInterface import com.coderzheaven.tutorialprojects.callback.UsersCallback import com.coderzheaven.tutorialprojects.models.User import com.coderzheaven.tutorialprojects.models.UserError import com.coderzheaven.tutorialprojects.constants.Constants.Companion.UNKNOWN_ERROR import com.coderzheaven.tutorialprojects.constants.Constants.Companion.UNKNOWN_ERROR_CODE import com.coderzheaven.tutorialprojects.constants.Constants.Companion.USER_LOAD_FAILURE import retrofit2.Call import retrofit2.Callback import retrofit2.Response class UsersRepo(callback: UsersCallback) : Callback<List<User>> { private val usersCallback: UsersCallback = callback private val userError = UserError(UNKNOWN_ERROR_CODE, UNKNOWN_ERROR) fun getAllUsers() { usersCallback.onLoading(true) val request = RetrofitUtil.buildService(UsersInterface::class.java) val call = request.getUsers() call.enqueue(this) } override fun onResponse(call: Call<List<User>>, response: Response<List<User>>) { usersCallback.onLoading(false) if (null == response.body()) { usersCallback.onFailed(userError) return } if (response.isSuccessful) { usersCallback.onSuccess(response.body()!!) return } usersCallback.onFailed(userError) } override fun onFailure(call: Call<List<User>>, t: Throwable) { val userError = UserError(USER_LOAD_FAILURE, t.message.toString()) usersCallback.onFailed(userError) usersCallback.onLoading(false) } }
Okay. Now our Repo is ready.
Let’s create the ViewModel now.
Create a class named “UsersViewModel“.
- UsersViewModel has three MutableLiveData variables which will be observed by the Activity/Fragment.
userList will be notified when the service is returned with a proper user list.
userError will be notified when there is some error from the service.
userLoading will be notified when the service starts loading and ends loading.
In the above Repo class when the service is returned, the below method is called
usersCallback.onSuccess(response.body()!!)
which will return the calling UsersViewModel onSuccess method.
Similarly for the “onFailed” and “onLoading” callbacks.
override fun onSuccess(usersList: List<User>) { userList.postValue(usersList) }
UsersViewModel
package com.coderzheaven.tutorialprojects.view_model import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.coderzheaven.tutorialprojects.callback.UsersCallback import com.coderzheaven.tutorialprojects.models.User import com.coderzheaven.tutorialprojects.models.UserError import com.coderzheaven.tutorialprojects.repo.UsersRepo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch class UsersViewModel(application: Application) : AndroidViewModel(application), UsersCallback { var userList: MutableLiveData<List<User>> = MutableLiveData() var userError: MutableLiveData<UserError> = MutableLiveData() var userLoading: MutableLiveData<Boolean> = MutableLiveData() fun getUsers() { if (userLoading.value == true) { return } viewModelScope.launch(Dispatchers.IO) { UsersRepo(this@UsersViewModel).getAllUsers() } } override fun onSuccess(usersList: List<User>) { userList.postValue(usersList) } override fun onFailed(error: UserError) { userError.postValue(error) } override fun onLoading(loading: Boolean) { userLoading.postValue(loading) } }
Every ViewModel has a viewModelScope which you can use for Threading.
Here we are using the Default Thread property when calling the getUsers.
Now Let’s switch to the UI part.
UI
We are going to write an Activty where all the incoming data is displayed.
package com.coderzheaven.tutorialprojects.view import android.os.Bundle import android.view.View import android.widget.Button import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.coderzheaven.tutorialprojects.R import com.coderzheaven.tutorialprojects.adapter.ListAdapter import com.coderzheaven.tutorialprojects.models.User import com.coderzheaven.tutorialprojects.models.UserError import com.coderzheaven.tutorialprojects.view_model.UsersViewModel class MainActivity : AppCompatActivity(), View.OnClickListener { private lateinit var mListRecyclerView: RecyclerView private lateinit var progressBar: ProgressBar private lateinit var btnLoad: Button private lateinit var tvErrorMessage: TextView private lateinit var viewModel: UsersViewModel private var mAdapter: ListAdapter? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) title = "MVVM" findViews() showErrorMessage(null) setListeners() } private fun findViews() { mListRecyclerView = findViewById(R.id.list_recycler_view) btnLoad = findViewById(R.id.btnLoad) progressBar = findViewById(R.id.progress) tvErrorMessage = findViewById(R.id.tvErrorMessage) btnLoad.setOnClickListener(this) } private fun setListeners() { viewModel = ViewModelProvider(this).get(UsersViewModel::class.java) viewModel.userList.observe(this, Observer(onUsersListReceived())) viewModel.userError.observe(this, Observer(onUserError())) viewModel.userLoading.observe(this, Observer(onLoading())) viewModel.getUsers() } private fun onUsersListReceived() = { users: List<User> -> setList(users) } private fun onLoading() = { loading: Boolean -> showLoading(loading) } private fun onUserError() = { userError: UserError -> showErrorMessage(userError.message) } private fun showErrorMessage(message: String?) { tvErrorMessage.visibility = if (null != message) View.VISIBLE else View.GONE tvErrorMessage.text = message } private fun showLoading(show: Boolean) { progressBar.visibility = if (show) View.VISIBLE else View.GONE } private fun setList(users: List<User>) { if (null != mAdapter) { mAdapter?.notifyDataSetChanged() return } mAdapter = ListAdapter(users) mListRecyclerView.adapter = mAdapter mListRecyclerView.layoutManager = LinearLayoutManager(this) } override fun onClick(v: View?) { if (v!!.id == R.id.btnLoad) { showErrorMessage(null) viewModel.getUsers() } } }
Here the setListeners() method registers the ViewModel and the observers.
viewModel = ViewModelProvider(this).get(UsersViewModel::class.java) viewModel.userList.observe(this, Observer(onUsersListReceived())) viewModel.userError.observe(this, Observer(onUserError())) viewModel.userLoading.observe(this, Observer(onLoading())) viewModel.getUsers()
The UsersiewModel has the LiveData variables such as the userList, userError and the userLoading.
When the viewModel calls postValue in one of the values based on the response from the Server, the observers in the MainActivity will be Notified.
The Observers here are “onUsersListReceived”, “onUserError” and “onLoading” respectively.
Once we revceive the data in the MainActivity, we can easily update the UI and thats it!!!.
We have mastered the MVVM pattern in Android.
The code for the RecyclerView adapter and MainActivity layout is in the github repo below.
Source Code
https://github.com/MrVipinVijayan/AndroidTutorialsMVVM
Please leave your valuable comments below.
Thanks for reading.