MVVM (Model View ViewModel) Architecture Pattern in Android

Beyond Key
Beyond Key
Published in
6 min readSep 29, 2022

In this blog, we are going to learn about the MVVM architecture in Android, and then we will build a project with MVVM architecture.

MVVM Architecture

MVVM architecture is a Model-View-ViewModel architecture that removes the tight coupling between each component. Most importantly, in this architecture, the children don’t have the direct reference to the parent, they only have the reference by observables.

Android UI Architecture

Architecture

  • Model: It represents the data and the business logic of the Android Application. It consists of the business logic — local and remote data source, model classes, repository.
  • View: It consists of the UI Code (Activity, Fragment), XML. It sends the user action to the ViewModel but does not get the response back directly. To get the response, it has to subscribe to the observables which ViewModel exposes to it.
  • ViewModel: It is a bridge between the View and Model (business logic). It does not have any clue which View has to use it as it does not have a direct reference to the View. So basically, the ViewModel should not be aware of the view who is interacting with. It interacts with the Model and exposes the observable that can be observed by the View.
  • The model defines the Data Related Logic (DRL), which some application operates with. It directly controls the data & fundamental behaviors, separate from the User Interface (UI). View provides output representation of information, i.e. UI. Controller generates an interface to connect two previous components. It operates straight to its user by receiving input data & performing appropriate operations with the model layer and view layer.

Let’s understand the concept by example
Setup the project:

The package in the project will look like below:

Add dependencies:

// Added Dependenciesimplementation "androidx.recyclerview:recyclerview:1.1.0"implementation 'android.arch.lifecycle:extensions:1.1.1'implementation 'com.github.bumptech.glide:glide:4.9.0'implementation 'com.amitshekhar.android:rx2-android-networking:1.0.2'// retrofitimplementation 'com.squareup.retrofit2:retrofit:2.9.0'// GSONimplementation 'com.squareup.retrofit2:converter-gson:2.9.0'

Add Internet permission inside manifest file:

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

Create the sealed class for manage the status from the API, this will create inside the utils package

package com.beyondkey.framework.mvvm.utils

sealed class Status(type : String) {
object SUCCESS : Status(“SUCCESS”)
object ERROR: Status(“ERROR”)
object LOADING : Status(“LOADING”)
}

Create the Resource class inside the utils to manage the status code Success, Error, Loading.

package com.beyondkey.framework.mvvm.utilsdata class Resource<out T>(val status: Status, val data: T?, val message: String?) {    companion object {        fun <T> success(data: T?): Resource<T> {            return Resource(Status.SUCCESS, data, null)        }        fun <T> error(msg: String, data: T?): Resource<T> {            return Resource(Status.ERROR, data, msg)        }        fun <T> loading(data: T?): Resource<T> {            return Resource(Status.LOADING, data, null)        }    }}

Create the new package data where we kept our api, model and repository.

Package — api (Inside the data package) We will create ApiConnection, ApiInterface and RetrofitClient class for the API calling.

ApiConnection:

package com.beyondkey.framework.mvvm.data.apiimport com.beyondkey.framework.mvvm.data.api.RetrofitClient.getRetrofiltClientimport com.beyondkey.framework.mvvm.data.model.Userimport retrofit2.Callobject ApiConnection {    fun getUser():Call<List<User>>{        return getRetrofiltClient().create(ApiInterface::class.java).getUser()    }}}

ApiInterface:

package com.beyondkey.framework.mvvm.data.apiimport com.beyondkey.framework.mvvm.data.model.Userimport retrofit2.Callimport retrofit2.http.GETimport retrofit2.http.Queryinterface ApiInterface {    @GET("/users")    fun getUser(    ): Call<List<User>>}

RetrofitClient:

package com.beyondkey.framework.mvvm.data.apiimport com.google.gson.Gsonimport com.google.gson.GsonBuilderimport okhttp3.OkHttpClientimport retrofit2.Retrofitimport retrofit2.converter.gson.GsonConverterFactoryimport java.util.concurrent.TimeUnitobject RetrofitClient {    val baseUrl = "https://5e510330f2c0d300147c034c.mockapi.io"    private var retrofitClient: Retrofit? = null    private val CONNECT_TIMEOUT_MULTIPLIER = 1    private val DEFAULT_CONNECT_TIMEOUT_IN_SEC = 300    private val DEFAULT_WRITE_TIMEOUT_IN_SEC = 300    private val DEFAULT_READ_TIMEOUT_IN_SEC = 300    fun getRetrofiltClient(): Retrofit {        if (retrofitClient == null) {            val gson: Gson = GsonBuilder()                .setLenient()                .create()            retrofitClient = Retrofit.Builder()                .baseUrl(baseUrl)                .addConverterFactory(GsonConverterFactory.create(gson))                .client(getOkHttpClientBuilder().build())                .build()        }        return retrofitClient as Retrofit    }    open fun getOkHttpClientBuilder(): OkHttpClient.Builder {        /*OkHttp client builder*/        val oktHttpClientBuilder = OkHttpClient.Builder()            .connectTimeout(                (CONNECT_TIMEOUT_MULTIPLIER * DEFAULT_CONNECT_TIMEOUT_IN_SEC).toLong(),                TimeUnit.SECONDS            )            .writeTimeout(                (CONNECT_TIMEOUT_MULTIPLIER * DEFAULT_WRITE_TIMEOUT_IN_SEC).toLong(),                TimeUnit.SECONDS            )            .readTimeout(                (CONNECT_TIMEOUT_MULTIPLIER * DEFAULT_READ_TIMEOUT_IN_SEC).toLong(),                TimeUnit.SECONDS            )        return oktHttpClientBuilder    }}

Package — Model (Inside the data package)
Create User class according to the API response

package com.beyondkey.framework.mvvm.data.modeldata class User(    val id: Int,    val name: String,    val email: String,    val avatar: String)

Package — repository (UserRepository — Inside the data package)

This class is communicated with the remote data source

package com.beyondkey.framework.mvvm.data.repositoryimport androidx.lifecycle.MutableLiveDataimport com.beyondkey.framework.mvvm.data.api.ApiConnectionimport com.beyondkey.framework.mvvm.data.model.Userimport com.beyondkey.framework.mvvm.utils.Resourceimport retrofit2.Callimport retrofit2.Callbackimport retrofit2.Responseclass UserRepository {    fun getUsers(data: MutableLiveData<Resource<List<User>>> = MutableLiveData<Resource<List<User>>>()) {        ApiConnection.getUser().enqueue(object : Callback<List<User>> {            override fun onResponse(                call: Call<List<User>>,                response: Response<List<User>>            ) {                val getAllTypeUserRoleRes: List<User>? =                    response.body()                data.postValue(Resource.success(getAllTypeUserRoleRes))            }            override fun onFailure(call: Call<List<User>>, t: Throwable) {                data.postValue(Resource.error("Something Went Wrong", null))            }        })    }}

Now create the UI package

ui → base

main

Now create the ViewModelFactory class inside the base package

package com.beyondkey.framework.mvvm.ui.baseimport androidx.lifecycle.ViewModelimport androidx.lifecycle.ViewModelProviderimport com.beyondkey.framework.mvvm.data.repository.UserRepositoryimport com.beyondkey.framework.mvvm.ui.main.viewmodel.MainViewModelclass ViewModelFactory(private val userRepository: UserRepository) : ViewModelProvider.Factory {    override fun <T : ViewModel?> create(modelClass: Class<T>): T {        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {            return MainViewModel(UserRepository()) as T        }        throw IllegalArgumentException("Unknown class name")    }}

Inside the main package we will create 3 other package adapter, view, viewModel.

main → adapter

view

viewModel

Create the MainActivity inside the view.

package com.beyondkey.framework.mvvm.ui.main.viewimport android.os.Bundleimport android.util.Logimport android.view.Viewimport android.widget.Toastimport androidx.appcompat.app.AppCompatActivityimport androidx.lifecycle.ViewModelProvidersimport androidx.recyclerview.widget.DividerItemDecorationimport androidx.recyclerview.widget.LinearLayoutManagerimport com.beyondkey.framework.mvvm.Rimport com.beyondkey.framework.mvvm.data.model.Userimport com.beyondkey.framework.mvvm.data.repository.UserRepositoryimport com.beyondkey.framework.mvvm.ui.base.ViewModelFactoryimport com.beyondkey.framework.mvvm.ui.main.adapter.MainAdapterimport com.beyondkey.framework.mvvm.ui.main.viewmodel.MainViewModelimport com.beyondkey.framework.mvvm.utils.Statusimport kotlinx.android.synthetic.main.activity_main.*class MainActivity : AppCompatActivity() {    private lateinit var mainViewModel: MainViewModel    private lateinit var adapter: MainAdapter    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main)        setupUI()        setupViewModel()        setupObserver()    }    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 setupObserver() {        mainViewModel.getUsers().observe(this) {            when (it.status) {                Status.SUCCESS -> {                    progressBar.visibility = View.GONE                    it.data?.let { users -> renderList(users) }                    recyclerView.visibility = View.VISIBLE                }                Status.LOADING -> {                    progressBar.visibility = View.VISIBLE                    recyclerView.visibility = View.GONE                }                Status.ERROR -> {                    //Handle Error                    progressBar.visibility = View.GONE                    Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()                }            }        }    }    private fun renderList(users: List<User>) {        adapter.addData(users)        adapter.notifyDataSetChanged()    }    private fun setupViewModel() {        val repo = UserRepository()        mainViewModel = ViewModelProviders.of(            this,            ViewModelFactory(repo)        ).get(MainViewModel::class.java)    }}

Now, let’s set up the XML layout. In the layout folder, update the activity_main.xml with the following code:

<?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>

Create the class MainViewModel inside the viewmodel package. Here, we have used LiveData for observe the data

package com.beyondkey.framework.mvvm.ui.main.viewmodelimport androidx.lifecycle.LiveDataimport androidx.lifecycle.MutableLiveDataimport androidx.lifecycle.ViewModelimport com.beyondkey.framework.mvvm.data.model.Userimport com.beyondkey.framework.mvvm.data.repository.UserRepositoryimport com.beyondkey.framework.mvvm.utils.Resourceclass MainViewModel(private val mainRepository: UserRepository) : ViewModel() {    private val users = MutableLiveData<Resource<List<User>>>()    init {        fetchUsers()    }    private fun fetchUsers() {        users.postValue(Resource.loading(null))        mainRepository.getUsers(users)    }    fun getUsers(): LiveData<Resource<List<User>>> {        return users    }}

Create the class MainAdapter inside the adapter package.

package com.beyondkey.framework.mvvm.ui.main.adapterimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport androidx.recyclerview.widget.RecyclerViewimport com.bumptech.glide.Glideimport com.beyondkey.framework.mvvm.Rimport com.beyondkey.framework.mvvm.data.model.Userimport kotlinx.android.synthetic.main.item_layout.view.*class MainAdapter(    private val users: ArrayList<User>) : RecyclerView.Adapter<MainAdapter.DataViewHolder>() {    class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {        fun bind(user: User) {            itemView.textViewUserName.text = user.name            itemView.textViewUserEmail.text = user.email            Glide.with(itemView.imageViewAvatar.context)                .load(user.avatar)                .into(itemView.imageViewAvatar)        }    }    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =        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 addData(list: List<User>) {        users.addAll(list)    }}

Now, let’s set up the XML layout. In the layout folder, update the item_layout.xml with the following code:

<?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:id="@+id/container"    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" />    <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_marginLeft="8dp"        android:layout_marginTop="4dp"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"        app:layout_constraintTop_toTopOf="parent"        tools:text="Beyond key" />    <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="Beyond key" /></androidx.constraintlayout.widget.ConstraintLayout>

Advantages of Using Clean Architecture

· Your code is even more easily testable than with plain MVVM.

· Your code is further decoupled (the biggest advantage.)

· The package structure is even easier to navigate.

· The project is even easier to maintain.

· Your team can add new features even more quickly.

Repository class isolates the data sources from the rest of the app and provides a clean API for data access to the rest of the app. Using a repository class ensures this code is separate from the ViewModel class, and is a recommended best practice for code separation and architecture.
Implementing repository pattern will create a data access layer that can give us pain-less data access. Not only that, by using this design pattern will help us to centralize all the data access that cause reduced boilerplate and duplicated code in the app.

--

--

Beyond Key
Beyond Key

Beyond Key is a Microsoft Solutions Partner company serving clients globally.