mvvm with retrofit and recyclerview poster

MVVM With Retrofit and Recyclerview in Kotlin [Example]

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 a direct reference to the parent, they only have the reference by observables.

What is MVVM?

MVVM stands for Model, View, ViewModel.

MVVM architecture diagram

Model:

This holds the data of the application. It cannot directly talk to the View. Generally, it’s recommended to expose the data to the ViewModel through Observables

View:

It represents the UI of the application devoid of any Application Logic. It observes the ViewModel.

ViewModel:

It acts as a link between the Model and the View. It’s responsible for transforming the data from the Model. It provides data streams to the View. It also uses hooks or callbacks to update the View. It’ll ask for the data from the Model.

To learn more about ViewModel check the official document.

ViewModel LifeCycle scope

This is all about the MVVM, now let’s move to the implementation part of it.

Adding dependencies for MVVM, Retrofit, and Recyclerview

Add the following dependencies to your app-level build.gradle.

//ViewModel and livedata
        implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
        //Retrofit
        implementation 'com.google.code.gson:gson:2.8.6'
        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'
        kapt 'com.github.bumptech.glide:compiler:4.12.0'

Also, don’t forget to add internet permission in your manifeats.xml file.

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

Set up the Data Layer

In the data layer, we have to prepare the model for the data, and an API call needs to be implemented. In this example, I am using a Repository pattern to handle the data.

Creating Model class

I am using the “https://www.howtodoandroid.com/movielist.json” API to fetch the data.

For the response data, First, we need to create a model class.

Model.kt

    data class Movie(val name: String, val imageUrl: String, val category: String, val desc: String)

Setting up Retrofit

Retrofit is a “Type-safe HTTP client for Android and Java”.

I have explained retrofit in another post.

Retrofit android example kotlin[step by step]

First, create the interface for the API call definition.

interface RetrofitService {
        @GET("movielist.json")
        fun getAllMovies(): Call<List<Movie>>
    }

Next, Create the Retrofit service instance using the retrofit.

Both are part of the same class. RetrofitService.kt

interface RetrofitService {
        @GET("movielist.json")
        fun getAllMovies() : Call<List<Movie>>

        companion object {
            var retrofitService: RetrofitService? = null

            fun getInstance() : RetrofitService {
                if (retrofitService == null) {
                    val retrofit = Retrofit.Builder()
                        .baseUrl("https://www.howtodoandroid.com/")
                        .addConverterFactory(GsonConverterFactory.create())
                        .build()
                    retrofitService = retrofit.create(RetrofitService::class.java)
                }
                return retrofitService!!
            }
        }
    }

Setup Data Repository

I am using a repository pattern to handle the data from API. In the repository class, we need to pass the retrofit service instance to perform the network call. We don’t need to handle the response here in the repository. That will be part of the ViewModel.

MainRepository.kt

class MainRepository constructor(private val retrofitService: RetrofitService) {
        fun getAllMovies() = retrofitService.getAllMovies()
    }

Setup the ViewModel

In the ViewModel setup, We need to create a class and extend the ViewModel. ViewModel class has business logic and API call implementations. In the ViewModel constructor, we need to pass the data repository to handle the data.

MainViewModel.kt

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

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

        fun getAllMovies() {
            val response = repository.getAllMovies()
            response.enqueue(object : Callback<List<Movie>> {
                override fun onResponse(call: Call<List<Movie>>, response: Response<List<Movie>>) {
                    movieList.postValue(response.body())
                }
                override fun onFailure(call: Call<List<Movie>>, t: Throwable) {
                    errorMessage.postValue(t.message)
                }
            })
        }
    }

We are using Live data to update the data to UI.

Live Data

Since LiveData respects Android Lifecycle, this means it will not invoke its observer callback unless activity or fragment is received onStart() but did not accept onStop() Adding to this, LiveData will also automatically remove the observer when its host receives onDestroy().

ViewModel Factory

We can not create ViewModel on our own. We need the ViewModelProviders utility provided by Android to create ViewModels.

But ViewModelProviders can only instantiate ViewModels with the no-arg constructor.

So if I have a ViewModel with multiple arguments, then I need to use a Factory that I can pass to ViewModelProviders to use when an instance of MyViewModel is required.

MyViewModelFactory.kt

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.

First, we need to set up the recyclerview in our MainActivity. I have explained about recyclerview in another post.

Recyclerview Android Example

Create recyclerview in our main XML file.

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:layout_constraintTop_toTopOf="parent"
            tools:listitem="@layout/adapter_movie"
            tools:itemCount="5"
            app:layout_constraintEnd_toEndOf="parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
            app:layout_constraintStart_toStartOf="parent"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

Also, create an adapter for the recyclerview to set all the items into the recyclerview.

MainAdapter.kt:

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 = AdapterMovieBinding.inflate(inflater, parent, false)
            return MainViewHolder(binding)
        }
        override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
            val movie = movies[position]
           holder.binding.name.text = movie.name
            Glide.with(holder.itemView.context).load(movie.imageUrl).into(holder.binding.imageview)
        }
        override fun getItemCount(): Int {
            return movies.size
        }
    }

    class MainViewHolder(val binding: AdapterMovieBinding) : RecyclerView.ViewHolder(binding.root) {
    }

item layout for the adapter. **adapter_movie.xml**:

<?xml version="1.0" encoding="utf-8"?>
    <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_margin="8dp"
        android:elevation="8dp"
        app:cardCornerRadius="8dp">
        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/imageview"
            android:layout_width="match_parent"
            android:scaleType="centerCrop"
            android:layout_height="200dp" />
        <TextView
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:gravity="center_vertical"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="@tools:sample/full_names"
            android:background="#E1303F9F"
            android:paddingStart="8dp"
            android:textAppearance="@style/TextAppearance.AppCompat.Large" />
    </androidx.cardview.widget.CardView>

Once, all the recyclerview setup is completed. we need to create a ViewModel instance and call the API.

val viewModel = ViewModelProvider(this, MyViewModelFactory(MainRepository(retrofitService))).get(MainViewModel::class.java)

    viewModel.getAllMovies()

Finally, we need to observe the response from the API and update the UI.

MainActivity.kt:

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)
            setContentView(R.layout.activity_main)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
            viewModel = ViewModelProvider(this, MyViewModelFactory(MainRepository(retrofitService))).get(MainViewModel::class.java)
            binding.recyclerview.adapter = adapter
            viewModel.movieList.observe(this, Observer {
                Log.d(TAG, "onCreate: $it")
                adapter.setMovieList(it)
            })
            viewModel.errorMessage.observe(this, Observer {
            })
            viewModel.getAllMovies()
        }
    }

That’s all. Thanks for reading. You can download this example in GITHUB.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *


Latest Posts