Android MVVM Pattern with Retrofit in Kotlin

This post is about the MVVM pattern in Android, and how to build a project with the MVVM pattern using Retrofit.

 

What is MVVM?

  • MVVM architecture is a Model-View-ViewModel architecture.
  • It allows separating the user interface logic from the business (or the back-end) logic.
  • Its target is to keep UI code simple and free of app logic in order to make it easier to manage and test. 

MVVM has mainly the following layers:

Model:

The model represents the data and the business logic of the Application. It consists of the business logic – local and remote data source, model classes, repository.

View:

The view consists of the UI Code(Activity, Fragment), XML. The view role in this pattern is to observe (or subscribe to) a ViewModel observable to get data in order to update UI elements accordingly.

ViewModel

ViewModel interacts with the model and also prepares observable(s) that can be observed by a View. 
One of the important implementation strategies of this layer is to decouple it from the View, i.e, ViewModel should not be aware of the view who is interacting with.

Creating new project

1 . Create a new project by going to File ⇒ New Android Project, select Empty Activity, provide app name, select language to kotlin and then finally click on finish.

2 . Open app-level build.gradle file and add the below changes:

  • Add the below libraries under the dependencies section:
dependencies {
//ViewModel and livedata
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"

//Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

//Glide
implementation 'com.github.bumptech.glide:glide:4.12.0'
implementation 'com.github.bumptech.glide:compiler:4.12.0'
}
  • Inside the android block, add viewBinding block with property enabled to true.
viewBinding {
enabled = true
}
  • Finally, click on Sync Now.

3. Open the AndroidManifest.xml file and add the internet permission above the application element.

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

Setup the Data Layer

In the data layer, we have to prepare the model for the data, and an API call needs to be implemented. 

Creating Model class

I will use “https://fake-movie-database-api.herokuapp.com/api?s=batman” API to fetch the data.

The API JSON response will be like this.

{
"Search": [
{
"imdbID": "tt0096895",
"Title": "Batman",
"Year": "1989",
"Poster": "https://images-na.ssl-images-amazon.com/images/M/MV5BMTYwNjAyODIyMF5BMl5BanBnXkFtZTYwNDMwMDk2._V1_.jpg"
},
{
"imdbID": "tt0468569",
"Title": "The Dark Knight",
"Year": "2008",
"Poster": "https://ia.media-imdb.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_UX182_CR0,0,182,268_AL_.jpg"
},
....
]
}

For the response data, we need to create the below two model classes.

MovieList.kt

import com.c1ctech.mvvmwithnetworksource.Movie
import com.google.gson.annotations.SerializedName

data class MovieList(@SerializedName("Search") val mList : List<Movie>)

Movie.kt

import com.google.gson.annotations.SerializedName

data class Movie(
@SerializedName("Title") val title: String,
@SerializedName("Poster") val poster: String,
val imdbID: String,
@SerializedName("Year") val year: String
)
Setting up Retrofit

Create an interface RetrofitService for the API call.

RetrofitService.kt

import com.c1ctech.mvvmwithnetworksource.model.MovieList
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET

interface RetrofitService {
@GET("api?s=batman")
fun getAllMovies(): Call<MovieList>

companion object {

var retrofitService: RetrofitService? = null

//Create the Retrofit service instance using the retrofit.
fun getInstance(): RetrofitService {

if (retrofitService == null) {
val retrofit = Retrofit.Builder()
.baseUrl("https://fake-movie-database-api.herokuapp.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofitService = retrofit.create(RetrofitService::class.java)
}
return retrofitService!!
}
}
}
Setup Data Repository

Inside the below repository class, we need to pass the retrofit service instance to perform the network call. The repository class will only interact with the network source, the response of the network call we will handle later in ViewModel.

MainRepository.kt

import com.c1ctech.mvvmwithnetworksource.RetrofitService

class MainRepository constructor(private val retrofitService: RetrofitService) {

fun getAllMovies() = retrofitService.getAllMovies()
}

Setup the ViewModel

The MainViewModel class extends the ViewModel. 

In the ViewModel constructor, we need to pass the data repository to handle the response from the API call.

It uses LiveData to update the data to UI. LiveData only updates app component observers that are in an active lifecycle state.

MainViewModel.kt

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.c1ctech.mvvmwithnetworksource.Movie
import com.c1ctech.mvvmwithnetworksource.model.MovieList
import com.c1ctech.mvvmwithnetworksource.repository.MainRepository
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class MainViewModel(private val repository: MainRepository) : ViewModel() {

val movieList = MutableLiveData<List<Movie>>()
val errorMessage = MutableLiveData<String>()

fun getAllMovies() {
val response = repository.getAllMovies()
response.enqueue(object : Callback<MovieList> {
override fun onResponse(call: Call<MovieList>, response: Response<MovieList>) {
movieList.postValue(response.body()?.mList)
}

override fun onFailure(call: Call<MovieList>, t: Throwable) {
errorMessage.postValue(t.message)
}
})
}
}

ViewModel Factory

To create ViewModel we have ViewModelProviders utility provided by Android. But ViewModelProviders can only instantiate ViewModels with the no-arg constructor.
So to create a ViewModel with multiple arguments, we need to use a Factory that we can pass to ViewModelProviders to use when an instance of MyViewModel is required.

MyViewModelFactory.kt

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.c1ctech.mvvmwithnetworksource.repository.MainRepository
import com.c1ctech.mvvmwithnetworksource.viewmodel.MainViewModel

class MyViewModelFactory constructor(private val repository: MainRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
MainViewModel(this.repository) as T
} else {
throw IllegalArgumentException("ViewModel Not Found")
}
}
}

Setting up the UI

In the UI part, We need to create an instance of the ViewModel and observe the API response. Based on the API response we need to update the UI.

Creating Activity Layout file

The below layout file contains a RecyclerView.

activity_main.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="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="5"
tools:listitem="@layout/layout_rv_item" />

</androidx.constraintlayout.widget.ConstraintLayout>
Creating Adapter Layout file

The below layout file represents the UI of each item of recyclerView.

layout_rv_item.xml

<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:elevation="8dp"
app:cardCornerRadius="8dp">

<ImageView
android:id="@+id/moviePoster"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/movieTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/purple_700"
android:gravity="center_vertical"
android:padding="3dp"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@android:color/white" />

</androidx.cardview.widget.CardView>
Creating Adapter class

The MainAdapter is an adapter class for the recycler view to set all the items into recycler view.

MainAdapter.kt

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.c1ctech.mvvmwithnetworksource.databinding.LayoutRvItemBinding

class MainAdapter : RecyclerView.Adapter<MainViewHolder>() {

var movies = mutableListOf<Movie>()

fun setMovieList(movies: List<Movie>) {
this.movies = movies.toMutableList()
notifyDataSetChanged()
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
val inflater = LayoutInflater.from(parent.context)

val binding = LayoutRvItemBinding.inflate(inflater, parent, false)
return MainViewHolder(binding)
}

override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
val movie = movies[position]
holder.binding.movieTitle.text = movie.title
Glide.with(holder.itemView.context).load(movie.poster).placeholder(R.drawable.placeholder)
.into(holder.binding.moviePoster)

}

override fun getItemCount(): Int {
return movies.size
}
}

class MainViewHolder(val binding: LayoutRvItemBinding) : RecyclerView.ViewHolder(binding.root) {}
Creating MainActivity File

Inside MainActivity, we will observe the response from the API and update the UI.

MainActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.c1ctech.mvvmwithnetworksource.databinding.ActivityMainBinding
import com.c1ctech.mvvmwithnetworksource.repository.MainRepository
import com.c1ctech.mvvmwithnetworksource.viewmodel.MainViewModel

class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
private lateinit var binding: ActivityMainBinding

lateinit var viewModel: MainViewModel

private val retrofitService = RetrofitService.getInstance()
val adapter = MainAdapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

//get viewmodel instance using MyViewModelFactory
viewModel =
ViewModelProvider(this, MyViewModelFactory(MainRepository(retrofitService))).get(
MainViewModel::class.java
)

//set recyclerview adapter
binding.recyclerview.adapter = adapter

viewModel.movieList.observe(this, Observer {
Log.d(TAG, "movieList: $it")
adapter.setMovieList(it)
})

viewModel.errorMessage.observe(this, Observer {
Log.d(TAG, "errorMessage: $it")
})

viewModel.getAllMovies()
}
}

When you run the app it will look like this:

 

Leave a Reply