Android MVVM Architecture Integration using Kotlin
Model is POJO class which represents logic associated with application data. View means UI layout which informs ViewModel about user’s actions. ViewModel is behavior model of view which interacts with model to describe state of data.
LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.
ViewModel is created to store and manage UI-related data in a lifecycle conscious way. It allows data to survive configuration changes such as screen rotations.
In this android mvvm example, We are using retrofit library to fetch data from api. If you are new to retrofit library then you can refer this article.
Add the below dependencies in your app level build.gradle file
dependencies { implementation "android.arch.lifecycle:extensions:1.1.1" annotationProcessor "android.arch.lifecycle:compiler:1.1.1" implementation 'com.squareup.retrofit2:retrofit:2.5.0' implementation 'com.squareup.retrofit2:converter-gson:2.5.0' implementation 'com.android.support:recyclerview-v7:28.1.1' implementation 'com.android.support:cardview-v7:28.1.1' }
Create ApiService interface to define API methods
package com.example.mvvmsample.api import com.example.mvvmsample.models.User import retrofit2.Call import retrofit2.http.GET interface ApiService { @GET("users") fun getUsers(): Call<MutableList<User>> }
Create ApiClient class to call API
Using RetrofitBuilder class, We can get retrofit instance with base url and httpclient. Retrofit2 does not provide any converter. So for JSON mapping, we need to use Gson converter by adding it into addConverterFactory method.
package com.example.mvvmsample.api import com.google.gson.Gson import com.google.gson.GsonBuilder import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory object ApiClient { private const val BASE_URL: String = "https://jsonplaceholder.typicode.com/" private val gson : Gson by lazy { GsonBuilder().setLenient().create() } private val httpClient : OkHttpClient by lazy { OkHttpClient.Builder().build() } private val retrofit : Retrofit by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(httpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .build() } val apiService : ApiService by lazy{ retrofit.create(ApiService::class.java) } }
Create Response classes to parse json data fetched from API
package com.example.mvvmsample.models import com.google.gson.annotations.SerializedName import java.io.Serializable data class User( @SerializedName("id") val id: Int? = null, @SerializedName("name") val name: String? = null, @SerializedName("username") val userName: String? = null, @SerializedName("email") val email: String? = null, @SerializedName("address") val addressObject : Address? = null, @SerializedName("phone") val phone: String? = null, @SerializedName("website") val website: String? = null ) : Serializable
package com.example.mvvmsample.models import com.google.gson.annotations.SerializedName import java.io.Serializable data class Address( @SerializedName("street") val street: String? = null, @SerializedName("suite") val suite: String? = null, @SerializedName("city") val city: String? = null, @SerializedName("zipcode") val zipCode: String? = null ) : Serializable
Create Util class to define functions for api calling
package com.example.mvvmsample.utils import android.R import android.app.Activity import android.content.Context import android.graphics.Color import android.net.ConnectivityManager import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.RelativeLayout import android.widget.TextView import android.widget.Toast object Utility { private var progressBar: ProgressBar? = null fun Context.isInternetAvailable(): Boolean { try { val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val netInfo = cm.activeNetworkInfo return if (netInfo != null && netInfo.isConnected) { true } else { showErrorToast("Internet not available. Please try again!!") false } } catch (e: Exception) { e.printStackTrace() } return false } fun Context.showErrorToast(message: String?) { try { val toast = Toast.makeText(this, message, Toast.LENGTH_LONG) // set message color val textView = toast.view.findViewById(android.R.id.message) as? TextView textView?.setTextColor(Color.WHITE) textView?.gravity = Gravity.CENTER // set background color toast.view.setBackgroundColor(Color.RED) toast.setGravity(Gravity.TOP or Gravity.FILL_HORIZONTAL, 0, 0) toast.show() } catch (e: Exception) { e.printStackTrace() } } // show progressbar fun Context.showProgressBar() { try { val layout = (this as? Activity)?.findViewById<View>(android.R.id.content)?.rootView as? ViewGroup progressBar = ProgressBar(this, null, R.attr.progressBarStyleLarge) progressBar?.let { it1 -> it1.isIndeterminate = true val params = RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT ) val rl = RelativeLayout(this) rl.gravity = Gravity.CENTER rl.addView(it1) layout?.addView(rl, params) it1.visibility = View.VISIBLE } } catch (e: Exception) { e.printStackTrace() } } // hide progressbar fun hideProgressBar() { try { progressBar?.let { it.visibility = View.GONE } } catch (e: Exception) { e.printStackTrace() } } }
Create a Repository class for android kotlin mvvm example
package com.example.mvvmsample.repositories import android.content.Context import android.util.Log import androidx.lifecycle.MutableLiveData import com.example.mvvmsample.api.ApiClient import com.example.mvvmsample.models.User import com.example.mvvmsample.utils.Utility.showProgressBar import com.example.mvvmsample.utils.Utility.hideProgressBar import retrofit2.Call import retrofit2.Callback import retrofit2.Response object UserRepository { fun getMutableLiveData(context: Context) : MutableLiveData<ArrayList<User>>{ val mutableLiveData = MutableLiveData<ArrayList<User>>() context.showProgressBar() ApiClient.apiService.getUsers().enqueue(object : Callback<MutableList<User>> { override fun onFailure(call: Call<MutableList<User>>, t: Throwable) { hideProgressBar() Log.e("error", t.localizedMessage) } override fun onResponse( call: Call<MutableList<User>>, response: Response<MutableList<User>> ) { hideProgressBar() val usersResponse = response.body() usersResponse?.let { mutableLiveData.value = it as ArrayList<User> } } }) return mutableLiveData } }
Create a ViewModel class for kotlin mvvm retrofit example
package com.example.mvvmsample.viewmodels import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.example.mvvmsample.repositories.UserRepository import com.example.mvvmsample.models.User import com.example.mvvmsample.utils.Utility.isInternetAvailable class UserViewModel(private val context: Context) : ViewModel() { private var listData = MutableLiveData<ArrayList<User>>() init{ val userRepository : UserRepository by lazy { UserRepository } if(context.isInternetAvailable()) { listData = userRepository.getMutableLiveData(context) } } fun getData() : MutableLiveData<ArrayList<User>>{ return listData }
Create a ViewModelFactory class to pass custom arguments with ViewModel
package com.example.mvvmsample.viewmodelsfactories import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.example.mvvmsample.viewmodels.UserViewModel class UserViewModelFactory(private val context: Context) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return UserViewModel(context) as T } }
Create layout for RecyclerView in Activity
<?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"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_main" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
Create layout row for list
<?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" app:cardCornerRadius="10dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:layout_marginStart="15dp" android:layout_marginEnd="15dp"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp"> <TextView android:id="@+id/txt_user_name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="22sp" android:textStyle="bold" /> <TextView android:id="@+id/txt_user_info1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/txt_user_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" /> <TextView android:id="@+id/txt_user_info2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/txt_user_info1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" /> <TextView android:id="@+id/txt_user_address" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/txt_user_info2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.cardview.widget.CardView>
Create adapter for mvvm architecture android kotlin
package com.example.mvvmsample.adapters import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.example.mvvmsample.R import com.example.mvvmsample.models.User class UsersAdapter(private val context: Context, private var list: MutableList<User>) : RecyclerView.Adapter<UsersAdapter.MyViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { val inflater = LayoutInflater.from(context) val view: View = inflater.inflate(R.layout.user_row,parent,false) return MyViewHolder(view) } override fun getItemCount(): Int { return list.size } override fun onBindViewHolder(holder: MyViewHolder, position: Int) { val user = list.get(position) holder.name?.text = user.name holder.info1?.text = user?.userName + " | " + user?.email holder.info2?.text = user?.phone + " | " + user?.website val addressObj = user.addressObject holder.address?.text = addressObj?.suite + "," + addressObj?.street + "," + addressObj?.city + "," + addressObj?.zipCode } class MyViewHolder(var view: View) : RecyclerView.ViewHolder(view){ var name: TextView? = null var info1: TextView? = null var info2: TextView? = null var address: TextView? = null init { name = view.findViewById(R.id.txt_user_name) info1 = view.findViewById(R.id.txt_user_info1) info2 = view.findViewById(R.id.txt_user_info2) address = view.findViewById(R.id.txt_user_address) } } }
Make changes in Activity for android kotlin mvvm architecture
package com.example.mvvmsample.activities import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import com.example.mvvmsample.R import com.example.mvvmsample.adapters.UsersAdapter import com.example.mvvmsample.models.User import com.example.mvvmsample.viewmodels.UserViewModel import com.example.mvvmsample.viewmodelsfactories.UserViewModelFactory import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { private lateinit var listUsers: MutableList<User> private lateinit var adapter: UsersAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) recycler_main.layoutManager = LinearLayoutManager(this@MainActivity) listUsers = mutableListOf<User>() adapter = UsersAdapter(this, listUsers ) recycler_main.adapter = adapter val userViewModel = ViewModelProviders.of(this,UserViewModelFactory(this)).get(UserViewModel::class.java) userViewModel.getData().observe(this,object:Observer<ArrayList<User>>{ override fun onChanged(t: ArrayList<User>?) { listUsers.clear() t?.let { listUsers.addAll(it) } adapter.notifyDataSetChanged() } }) } }
Well designed article with an easy to understand example
Very useful..
Great article
Excellent article for mvvm pattern
Good all information in one article
This is a great tip especially to those new to the blogosphere. Short but very accurate information… Thank you for sharing this one. A must read post!