Inducing MVVM to Existing Kotlin Code

4 min read

Inducing MVVM to Android Apps : Easy Android Programming

Dear readers welcome to an article about Inducing MVVM to Existing Kotlin Code. A new blog in the EASY ANDROID Programming Series.

In this article, we are going to learn how we are going to improve android application which we developed in our last blog. Before starting my article To set up an agenda for our discussion, below is the index of topic

  1. Introduction (Already done)
  2. Quotation
  3. Assumptions
  4. File Organisation
  5. Code Walkthrough
  6. MVVM structure
  7. MVVM usage with Retrofit
  8. Output
  9. Conclusion

Quotation

For the topic on improvement, I would like to quote an eminent personality, Mr. Roy T Bennet, in the book The Light In the Heart

Let the improvement of yourself keep you so busy that you have no time to criticize others.

Assumptions

We assume that our readers know git an SCM tool, a version control system. If not, you may visit our earlier blogs this and this.

Since we are going to improve the kotlin application, we expect our readers to have Android studio, so they can better understand.

Last but not the least, please get the code from GitHub source code repo. and check out the brmaster branch. We will be improving this code.

File organization and explanation

File Organisation : Inducing MVVM to Existing Kotlin Code
Android studio screenshot

When you checkout and open the brmaster branch from the repository, you will see the above code structure.

To know more about each file you may visit another blog later so that we remain on the topic.

We will be preparing for inducing MVVM so bit of reorganization and addition of few folders.

MVVM stands for Model View View Model, a must know, design pattern, that saves android developers from many strange app behaviors. Below image that shows various components in MVVM design pattern

Android MVVM Model : Easy Android Programmin
Image by Relsell Global

View and View model are bound using observers (no callback).

The Need for MVVM describes the problem with a callback.

We will be adding a few folders in the app under moviesretrofitdemo folder,

  1. viewmodels ( View models and related kt files will be kept there)
  2. repository (Singleton repository file will be kept there)
  3. utils (Utils, constants files will be kept there)
  4. businesslogic ( files related to business logic will be kept there)
  5. ui under businesslogic ( file related to UI will be kept there)

Also, you need to add the following files and folders

  1. pojo folders inside ui ( Model classes file will be kept here), Kindly create Movie.kt file under the ui folder.
  2. inside repository folder create MoviesRepository.kt class
  3. inside viewmodels folder create MoviesListFragmentViewModel.kt class

Note: All this is already done in source code repo. you just need to checkout the right branch. (brmvvmimprovment)

Please move your MainActivity.kt class to ui folder and Add a ListFragment inside the ui folder.

Code Walkthrough

Let’s start with a top-down approach, So mainactivity.kt is the first file

package com.relsellglobal.moviesretrofitdemo.businesslogic.ui

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.relsellglobal.moviesretrofitdemo.R

class MainActivity : AppCompatActivity(){


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        supportFragmentManager
            .beginTransaction()
            .replace(R.id.rootLayout,MoviesListFragment())
            .commit()



    }
}

Above we require MoviesListFragment

/*
 * Copyright (c) 2020. Relsell Global
 */

package com.relsellglobal.moviesretrofitdemo.businesslogic.ui

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.relsellglobal.moviesretrofitdemo.R
import com.relsellglobal.moviesretrofitdemo.businesslogic.ui.pojo.Movie
import com.relsellglobal.moviesretrofitdemo.viewmodels.MoviesListFragmentViewModel

/**
 * A fragment representing a list of Items.
 * Activities containing this fragment MUST implement the
 * [MoviesListFragment.OnListFragmentInteractionListener] interface.
 */
class MoviesListFragment : Fragment() {

    // TODO: Customize parameters
    private var columnCount = 1


    var moviesListRV : RecyclerView ?= null
    var adapter : MovieItemRecyclerViewAdapter ?= null

    var movieList = ArrayList<Movie>()


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_movie_item_list, container, false)

        moviesListRV = view.findViewById(R.id.list)

        return view
    }



    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        adapter = MovieItemRecyclerViewAdapter(movieList)
        moviesListRV?.layoutManager = LinearLayoutManager(activity)
        moviesListRV?.adapter = adapter

        val model = ViewModelProviders.of(this).get(MoviesListFragmentViewModel::class.java)

        model.fetchMoviesForYear(2001).observe(this, Observer {
            if (it != null && !it.isEmpty()) {
                movieList.addAll(it)
                adapter?.notifyDataSetChanged()
            }
        })

    }

//    interface OnListFragmentInteractionListener {
//        // TODO: Update argument type and name
//        fun onListFragmentInteraction(item: DummyItem?)
//    }

}

Besides the above, we need to add MovieItemRecyclerViewAdapter.kt class.

/*
 * Copyright (c) 2020. Relsell Global
 */

package com.relsellglobal.moviesretrofitdemo.businesslogic.ui


import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.relsellglobal.moviesretrofitdemo.R
import com.relsellglobal.moviesretrofitdemo.businesslogic.ui.dummy.DummyContent.DummyItem
import com.relsellglobal.moviesretrofitdemo.businesslogic.ui.pojo.Movie
import kotlinx.android.synthetic.main.fragment_movie_item.view.*

/**
 * [RecyclerView.Adapter] that can display a [DummyItem] and makes a call to the
 * specified [OnListFragmentInteractionListener].
 * TODO: Replace the implementation with code for your data type.
 */
class MovieItemRecyclerViewAdapter(
    private val mValues: List<Movie>
) : RecyclerView.Adapter<MovieItemRecyclerViewAdapter.ViewHolder>() {

    private val mOnClickListener: View.OnClickListener

    init {
        mOnClickListener = View.OnClickListener { v ->
            val item = v.tag as Movie
            // Notify the active callbacks interface (the activity, if the fragment is attached to
            // one) that an item has been selected.
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.fragment_movie_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = mValues[position]
        holder.nameTV.text = item.name
        holder.yearTV.text = item.year
//
//        with(holder.mView) {
//            tag = item
//            setOnClickListener(mOnClickListener)
//        }
    }

    override fun getItemCount(): Int = mValues.size

    inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) {
        val nameTV: TextView = mView.name
        val yearTV: TextView = mView.year

    }
}

In continuation, we need to add code for ViewModel class ie MoviesListFragmentViewModel.kt inside viewmodels.

/*
 * Copyright (c) 2020. Relsell Global
 */

package com.relsellglobal.moviesretrofitdemo.viewmodels

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.relsellglobal.moviesretrofitdemo.businesslogic.ui.pojo.Movie
import com.relsellglobal.moviesretrofitdemo.repository.MoviesRepository

class MoviesListFragmentViewModel : ViewModel() {
    private lateinit var moviesList : MutableLiveData<List<Movie>>

    fun fetchMoviesForYear(year : Int) : LiveData<List<Movie>> {
        if(!::moviesList.isInitialized){
            moviesList = MutableLiveData()
            moviesList = loadMovies(year)
        }
        return moviesList
    }

    private fun loadMovies(year : Int): MutableLiveData<List<Movie>> {
        return MoviesRepository.instance.fetchMoviesData(year)
    }
}

MoviesRepository.kt class is to be added further,

/*
 * Copyright (c) 2020. Relsell Global
 */

package com.relsellglobal.moviesretrofitdemo.repository

import androidx.lifecycle.MutableLiveData
import com.relsellglobal.moviesretrofitdemo.businesslogic.ui.pojo.Movie
import com.relsellglobal.moviesretrofitdemo.businesslogic.ui.pojo.MoviesResponse
import com.relsellglobal.moviesretrofitdemo.repository.moviesapi.MoviesService
import com.relsellglobal.moviesretrofitdemo.utils.AppConstants
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class MoviesRepository private constructor() {
    private  var BaseUrl = AppConstants.Http.baseUrl

    private object HOLDER {
        val INSTANCE = MoviesRepository()
    }

    companion object {
        val instance: MoviesRepository by lazy { HOLDER.INSTANCE }
    }

    fun fetchMoviesData(year : Int): MutableLiveData<List<Movie>> {

        val data = MutableLiveData<List<Movie>>()

        val retrofit = Retrofit.Builder()
            .baseUrl(BaseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        val service = retrofit.create(MoviesService::class.java)
        val call = service.getMoviesList(""+year)

        call.enqueue(object : Callback<MoviesResponse> {
            override fun onResponse(call: Call<MoviesResponse>, response: Response<MoviesResponse>) {
                if (response.code() == 200) {
                    val movieResponse = response.body()!!
                    data.value = movieResponse.moviesArr
//                    for( movie in movieResponse.moviesArr){
//                        Log.v("MoviesRepository", movie.name)
//                    }


                }
            }
            override fun onFailure(call: Call<MoviesResponse>, t: Throwable) {

            }
        })



        return data
    }


}

Add AppConstants.java class in the utils folder

/*
 * Copyright (c) 2020. Relsell Global
 */

package com.relsellglobal.moviesretrofitdemo.utils;

public class AppConstants {
    public interface Http {
        String baseUrl = "https://www.relsellglobal.in/tutorial_blogs/";
    }
}

Output

Finally, when you run checkout code on device or simulator you will see a similar screen

Output Image : Inducing MVVM to Existing android apps

Conclusion

This brings us to the end of the blog about Inducing MVVM to Existing Kotlin Code. For a single fragment app MVVM is overkill. But for larger apps, MVVM code structure really shines.

Thanks for reading.

Leave a Reply

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