MVVM -Kotlin -Dagger2 -RxJava2 -Retrofit in Android

Srinivasa Rao Makkena
11 min readJul 22, 2020

--

As part of the Google’s guidelines, MVVM architecture implementation is one. ViewModel is one activity lifecycle aware component among the multiple Google’s Jetpack components that google has implemented which make developers life easy. With this architecture, developer can write the clean code and complete decoupling of code between View and ViewModel, so that easy to write the unit test cases.

Suppose you want to display different offers/cards in your home page with different sizes and some cards with images and some without images and all the text sizes, text colors and images changing dynamically. How do we use Data-Binding to leverage this?

Things which can be learned in this?

  • How to represent different layout in the RecyclerView?
  • How to inject ViewModel classes using ViewModelFactory?
  • How do we use data-binding to append texts/text-colors/text-sizes dynamically from API?(easily)

This is mainly happen in the eCommerce/Telecommunication apps like Verizon or Walmart, so in their apps, they want to display some offers and those offers keep on changing without need for the new build to deployed. So that we can control the app offers and links to offers dynamically from the APIs/Content Management Systems. In this case, data-binding will really helpful along with LiveData to update those views when ever there is a change in data from APIs.

Dependencies required?

//Retrofit, GSON, OKhttp
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation 'com.google.code.gson:gson:2.8.5'
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
//Glide
implementation 'com.github.bumptech.glide:glide:4.8.0'


//RxJava
implementation "io.reactivex.rxjava2:rxjava:2.2.0"
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.1.1-SNAPSHOT'
//RecyclerView
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
//LifeCycle Aware Components- LiveData, ViewModel etc.
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0-alpha04"
annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.1.0-alpha04"
//Dagger2
implementation "com.google.dagger:dagger:$dagger2_version"
kapt "com.google.dagger:dagger-compiler:$dagger2_version"
compileOnly "org.glassfish:javax.annotation:3.1.1"

This is what we are going to build. Here everything is coming from the API response only. Which includes Text, Text Size, Text Color, Type of Card( Whether it has image or not), Text Color etc.

Folder Structure in Android Studio

Out of all of these, I have explained how to work with recycler view in my previous articles. Here, main concentration is on how to work with MVVM altogether with the above mentioned 3rd party libraries and dependency injection.

Data Flow

Yeah, this is very minute level discussion, but I just wanted to explain for the people who just started learning. If you go through the below codes and layouts, you will understand how this data flow works. Here, we are not instantiating any view, we are getting data and updating the views directly in xml layout only. That’s the beauty of Data-Binding and using this architecture.

  1. item_card.xml

It is having title, description with image as shown in the above UX (row3).

<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data>

<variable
name="viewModel"
type="com.srinivas.tmobile.viewmodel.CardViewModel" />
</data>

<androidx.cardview.widget.CardView
android:id="@+id/cardViewImageDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp">

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="0dp"
android:paddingRight="0dp">

<ImageView
android:id="@+id/card_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitXY"
android:visibility="visible"
app:itemImage="@{viewModel.getItemImage()}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/no_image_available"
app:layout_constraintBottom_toBottomOf="parent"
/>

<TextView
android:id="@+id/card_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:textColor="@{viewModel.getTitleTextColor()}"
android:textStyle="bold"
app:textSize="@{viewModel.getTitleTextSize()}"
android:layout_marginLeft="16dp"
app:layout_constraintBottom_toTopOf="@+id/card_body"
app:layout_constraintStart_toStartOf="parent"
app:mutableText="@{viewModel.getCardTitle()}" />

<TextView
android:id="@+id/card_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:layout_marginLeft="16dp"
app:textColor="@{viewModel.getDescriptionTextColor()}"
app:textSize="@{viewModel.getDescriptionTextSize()}"
app:layout_constraintBottom_toBottomOf="@id/card_image"
app:layout_constraintStart_toStartOf="parent"
app:mutableText="@{viewModel.getCardDescription()}" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>

If we observe properly, we were using same CardViewModel in each of these layouts to update the views with data-binding accordingly. item_card_text.xml is just to show the title (Hello, Welcome to App) that is first row in the above screenshot. item_card_text_description.xml( 2nd row with two textviews)

2. activity_main.xml is just recyclerview to display all these cards/offers.

<?xml version="1.0" encoding="utf-8"?>

<layout 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">

<data>

<variable
name="viewModel"
type="com.srinivas.tmobile.viewmodel.CardListViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<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"
app:mutableVisibility="@{viewModel.getLoadingVisibility()}" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listOfCards"
android:layout_width="0dp"
android:layout_height="0dp"
app:adapter="@{viewModel.getPostListAdapter()}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</layout>

There we get the curiosity to know about CardViewModel.kt class and from where did we get the mutableText/app:textSize/app:itemImage etc. properties.

Data-Binding

BindingAdapters.kt

If we see above layouts, there are some unknown properties like “app:adapter”, “app:mutableText” and others which were hi-lighted. We need to create binding adapter to use them like below. We need to assign the actual data directly here by using the MutableLiveData object (which will react every time the data changes). We can see them in the CardViewModel class.

//with this property, we just need to create instance of adapter class and assign it to this.@BindingAdapter("adapter")
fun setAdapter(view: RecyclerView, adapter: RecyclerView.Adapter<*>) {
view.adapter = adapter
}
//make view visible or invisible
@BindingAdapter("mutableVisibility")
fun setMutableVisibility(view: View, visibility: MutableLiveData<Int>?) {
val parentActivity: AppCompatActivity? = view.getParentActivity()
if (parentActivity != null && visibility != null) {
visibility.observe(
parentActivity,
Observer { value -> view.visibility = value ?: View.VISIBLE })
}
}
//binding image to ImageView using Glide library to download the image from internet

@BindingAdapter("itemImage")
fun loadImage(view: ImageView, imageUrl: String?) {
if (!imageUrl.isNullOrBlank()) {
val requestOptions = RequestOptions()
requestOptions.placeholder(R.drawable.no_image_available)
requestOptions.error(R.drawable.no_image_available)

Glide.with(view.context)
.load(imageUrl)
.apply(requestOptions)
.into(view)
}
}

CardViewModel.kt

In item_card.xml, we have defined variable instance for this view model, so that we can directly call the methods from this view model to update the views. Here, Card object is being passed from RecyclerView.Adapter class based on that layout type through bind() function. So, using that Card object we are updating the view accordingly.

class CardViewModel : BaseViewModel() {


private val cardTitle: MutableLiveData<String> = MutableLiveData<String>()
private val cardBody: MutableLiveData<String> = MutableLiveData<String>()
private val imageUrl: MutableLiveData<String> = MutableLiveData<String>()
private val cardTitleColor: MutableLiveData<String> = MutableLiveData<String>()
private val cardDescriptionColor: MutableLiveData<String> = MutableLiveData<String>()
private val textSizeTitle: MutableLiveData<Int> = MutableLiveData<Int>()
private val textSizeDescription: MutableLiveData<Int> = MutableLiveData<Int>()


fun bind(card: Card) {
card.cardType?.let {
cardTitle
?.value = card.cardType
}
//postBody?.value = card.card.title.value

when (card.cardType) {
"text" -> {
card.card?.let {
cardTitle
.value = card.card.value
cardTitleColor
.value = card.card.attributes.textColor
textSizeTitle
.value = card.card.attributes.font.size
}
}
"title_description" -> {
setAttributeValues(card)


}
"image_title_description" -> {
imageUrl.value = card.card.image.url
setAttributeValues(card)

}

}
}

private fun setAttributeValues(card: Card) {
cardTitle.value = card.card.title.value
cardBody
.value = card.card.description.value
cardTitleColor
.value = card.card.title.attributes.textColor
cardDescriptionColor
.value = card.card.description.attributes.textColor
textSizeTitle
.value = card.card.title.attributes.font.size
textSizeDescription
.value = card.card.description.attributes.font.size
}

/**
* update title dynamically
*/
fun getCardTitle(): MutableLiveData<String>? {
return cardTitle
}

/**
* update description dynamically
*/
fun getCardDescription(): MutableLiveData<String>? {
return cardBody
}

/**
* update image dynamically
*/
fun getItemImage(): MutableLiveData<String>? {
return imageUrl
}

/**
* update title text color dynamically
*/
fun getTitleTextColor():MutableLiveData<String>?{
return cardTitleColor;
}

/**
* update description text color dynamically
*/
fun getDescriptionTextColor():MutableLiveData<String>?{
return cardDescriptionColor;
}

/**
* update description size dynamically
*/
fun getDescriptionTextSize():MutableLiveData<Int>?{
return textSizeDescription;
}

/**
* update title size dynamically
*/
fun getTitleTextSize():MutableLiveData<Int>?{
return textSizeTitle;
}
}

CardListAdapter.kt

If you observe here, we are inflating 3 different layouts based on viewType that we are defining in getItemViewType() function. You will understand this clearly when you see the JSON file. In ViewHolder class we have function called bind(), that we are passing the CardViewModel to the item_card layout.

class CardListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private lateinit var cardList: List<Card>

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (viewType == 2) {
val binding: ItemCardTextDescriptionBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_card_text_description,
parent,
false
)
return ViewHolder2(binding)
} else if (viewType == 3) {
val binding: ItemCardBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_card,
parent,
false
)
return ViewHolder(binding)
} else {

val binding: ItemCardTextBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_card_text,
parent,
false
)
return ViewHolder1(binding)

}

}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val viewType:Int = getItemViewType(position);
when (viewType){
0,1 -> {
val holder1:ViewHolder1 = holder as ViewHolder1

holder1.bind(cardList[position])
}
2 -> {
val holder2:ViewHolder2 = holder as ViewHolder2

holder2.bind(cardList[position])
}
3 -> {
val holder3:ViewHolder = holder as ViewHolder

holder3.bind(cardList[position])
}

}


}

override fun getItemViewType(position: Int): Int {
when (cardList.get(position).cardType) {
"text" -> return 1
"title_description" -> return 2
"image_title_description" -> return 3
else -> return 0
}
}

override fun getItemCount(): Int {
return if (::cardList.isInitialized) cardList.size else 0
}

fun updatePostList(postList: List<Card>) {
this.cardList = postList
notifyDataSetChanged()
}

class ViewHolder(private val binding: ItemCardBinding) : RecyclerView.ViewHolder(binding.root) {
private val viewModel = CardViewModel()

fun bind(post: Card) {
viewModel.bind(post)
binding.viewModel = viewModel
}
}

class ViewHolder1(private val binding: ItemCardTextBinding) :
RecyclerView.ViewHolder(binding.root) {
private val viewModel = CardViewModel()

fun bind(post: Card) {
viewModel.bind(post)
binding.viewModel = viewModel
}
}

class ViewHolder2(private val binding: ItemCardTextDescriptionBinding) :
RecyclerView.ViewHolder(binding.root) {
private val viewModel = CardViewModel()

fun bind(post: Card) {
viewModel.bind(post)
binding.viewModel = viewModel
}
}
}

CardListViewModel.kt

So, Till now, we have updated view by assuming we have the data. But we don’t have data yet officially. That we need to query the API and get it and pass it to the Adapter, from there to the CardViewModel to set the MutableLiveData variables and there to the item_card layout to update the views.

class CardListViewModel:BaseViewModel() {

@Inject
lateinit var homeApi: HomeApi
val errorMessage: MutableLiveData<Int> = MutableLiveData()
val loadingVisibility: MutableLiveData<Int> = MutableLiveData()
val cardListAdapter: CardListAdapter = CardListAdapter()


private lateinit var disposable: Disposable


init{
loadCards()
}

private fun loadCards(){

disposable = homeApi.getCards().subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { onRetrieveCardListStart() }
.doOnTerminate { onRetrieveCardListFinish() }
.subscribe(
{
result -> onRetrieveCardListSuccess(result)
},
{
onRetrieveCardListError()
}

)


}

private fun onRetrieveCardListStart(){
loadingVisibility.value = View.VISIBLE
errorMessage.value = null
}

private fun onRetrieveCardListFinish(){
loadingVisibility.value = View.GONE
}

private fun onRetrieveCardListSuccess(result: HomeApiResponse){
val page = result.page
cardListAdapter
.updatePostList(page.cards)
}


private fun onRetrieveCardListError(){
errorMessage.value = R.string.error_msg
}

}

If we observe clearly, we have @Inject annotation, that means we are injecting that HomeApi Object using Dagger2 library.

We have API call here using RxJava and Retrofit libraries to get the data and updating Adapter once the api call success and once we get the data.

To learn more about the RxJava, Please refer to below articles written by me.

HomeApi.kt

We are calling this in the above loadCards() method in CardListViewModel. This can be doable with the help of Retrofit library. @GET, @POST, @PUT etc. methods available in Retrofit http client.

/**
* The interface which provides methods to get result of apis
*/
interface HomeApi {
/**
* Get the home cards list from the API
*/
@GET("/test/home")
fun getCards(): Observable<HomeApiResponse>


}

Dependency Injection?

@Module, @Component, @Inject, @Provides are important annotations available with Dagger2 library.

@Inject, is to inject the dependencies and will write this with constructors(Constructor Injection)/fields (Field Injection).

@Module, This is mainly to provide the dependencies for the classes/interfaces where we can’t access the constructor like interfaces/pre defined classes like Context, Retrofit, OkHttpClient etc. with the help of @Provides annotation.

@Component, as an interface between modules and actual classes where Dagger2 needs to provide those dependencies.

NetworkModule.kt

Object in Kotlin means Singleton class. Here, we have provide functions for Retrofit, HomeApi ( where we have @Inject annotation in the previous class CardListViewModel.kt is because Dagger2 is providing this instance whenever that class needed because of this here.

@Module
object
NetworkModule {
/**
* Provides the Get service implementation.
*
@param retrofit the Retrofit object used to instantiate the service
*
@return the Get service implementation.
*/
@Provides
@Reusable
@JvmStatic
internal fun provideHomeApi(retrofit: Retrofit): HomeApi {
return retrofit.create(HomeApi::class.java)
}

/**
* Provides the Retrofit object.
*
@return the Retrofit object
*/
@Provides
@Reusable
@JvmStatic
internal fun provideRetrofitInterface(okHttpClient:OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
.client(okHttpClient)
.build()
}

/**
* Provides OkHttpClient to set the time out and logging
*
@return the OkHttpClient object
*/

@Provides
@Singleton
fun provideOkhttpClient(): OkHttpClient {
val client = OkHttpClient.Builder()
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
client.addInterceptor(interceptor)
client.connectTimeout(20,TimeUnit.SECONDS)
client.readTimeout(20,TimeUnit.SECONDS)
return client.build()
}

}

ViewModelInjector.kt

Dagger2 appends Dagger in front of Component class and create instance for that component. Where it injects all those dependencies from module to the classes which are in need. Here, it will create DaggerViewModelInjector.

@Singleton
@Component(modules = [(NetworkModule::class)])
interface ViewModelInjector {
/**
* Injects required dependencies into the specified CardListViewModel.
*
@param cardListViewModel CardListViewModel in which to inject the dependencies
*/
fun inject(cardListViewModel: CardListViewModel)
/**
* Injects required dependencies into the specified CardViewModel.
*
@param cardViewModel CardViewModel in which to inject the dependencies
*/
fun inject(cardViewModel: CardViewModel)

@Component.Builder
interface Builder {
fun build(): ViewModelInjector

fun networkModule(networkModule: NetworkModule): Builder
}
}

So, basically here, Dagger needs to provide dependencies (Retrofit, OKHttpClient and HomeApi) in the CardListViewModel. Generally we can instantiate this in the Application class if we have more dependencies to provide to different classes.

So I created this in BaseViewModel class.

BaseViewModel.kt

abstract class BaseViewModel:ViewModel() {

private val injector: ViewModelInjector = DaggerViewModelInjector
.builder()
.networkModule(NetworkModule)
.build()

init {
inject()
}

/**
* Injects the required dependencies
*/
private fun inject() {
when (this) {
is CardListViewModel -> injector.inject(this)
is CardViewModel -> injector.inject(this)
}
}

}

Here, Thanks for reading till now. Everyone will have question here, ViewModel is a predefined class, how are we injecting the dependencies to those classes.

The solution is “ViewModelFactory”.

This is how we instantiate the actual ViewModel class in Activity/Fragment.

viewModel =
ViewModelProviders.of(this).get(CardListViewModel::class.java)

But, we can create custom ViewModelFactory and pass it through the of() method as below.

viewModel =
ViewModelProviders.of(this, ViewModelFactory(this)).get(CardListViewModel::class.java)

ViewModelProvider have the Factory interface extended with it. Through which we can pass any data if we want to pass that data to the ViewModel constructor or to inject any dependencies.

ViewModelFactory.kt

class ViewModelFactory(private val activity: AppCompatActivity): ViewModelProvider.Factory{
/**
* Here We can pass the dependencies needed for ViewModel like Room DB instance / data repository instance etc., Injectig ViewModel is different approach than regular approach
*/
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(CardListViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return CardListViewModel() as T
}
throw IllegalArgumentException("Unknown ViewModel class")

}
}

This leads finally to Activity class where we interact with CardListViewModel.kt

MainActivity.kt

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private lateinit var viewModel: CardListViewModel

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

binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.listOfCards.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)

viewModel =
ViewModelProviders.of(this, ViewModelFactory(this)).get(CardListViewModel::class.java)
viewModel.errorMessage.observe(this, Observer { errorMessage ->
if
(errorMessage != null) showError(errorMessage)
})
binding.viewModel = viewModel
}


private fun showError(@StringRes errorMessage: Int) {
this.toast(getString(R.string.error_msg));
}

}

Constants.kt

This is to define all the constants that we need. For example, BASE_URL= “http://www.google.com”

Extensions.kt

Any needed extension functions to the classes.

/**
* It is to get the Context of Parent Activity from the view
*/
fun View.getParentActivity(): AppCompatActivity?{
var context = this.context
while (context is ContextWrapper) {
if (context is AppCompatActivity) {
return context
}
context = context.baseContext
}
return null
}


/**
* Extension function to Toast class, you can call this from any class
*/

fun Context.toast(message: CharSequence) =
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()

With all these, you should be able to understand the flow and how to work with MVVM architecture.

M — Model — Data classes/entities if integrate with SQLite/RoomDB

V — View — All Activities and Fragments

VM — ViewModel — All view models where we can have business logic to get the data and logic to update the data with views.

Here, View purpose is only to inflate the view. ViewModel will take care of updating views. And Model will be updated once we get any data changes from API. And that will pass data to the ViewModel and ViewModel will pass data to View. This is unidirectional data flow, So, there is no tight coupling between View and ViewModel. This is very much useful to right the Unit Test Cases.

You Just need to change the API end point and layout according to the requirements.

Thanks for going through all this cool stuff. Please comment if there are any questions.

https://github.com/SrinivasaRaoMakkena/PromotionsPage

Happy Learning!

Thanks.

--

--