MVVM in a modern Android App

The purpose of this post is to document the usage of MVVM with AndroidX components and Google’s Jetpack framework with a real-world use case.

With Model-View-ViewModel (MVVM), the UI is is loosely coupled to the data it presents and the user interacts with. Working with the Android SDK, the developer usually has to directly interact with the controls, where data is to be presented or updated. This is considered tight coupling.

// In a View's code behind
val nameTextView: TextView = findViewById(R.id.nameTextView)
nameTextView.text = "Name"

Jetpack is Google’s answer to introducing data-binding, or in other words, loose coupling between the UI and its data (stored in ViewModels) to the Android SDK.

Dependencies

For this post, the basic assumption is that the entire app is based on AndroidX for UI components and Jetpack.

The dependencies used to include the libraries for AndroidX and Jetpack in a project of mine are as follows (extract from the module’s build.gradle):


...

dependencies {  
  
    // androidx
    implementation 'androidx.core:core-ktx:${versions.core}'  
    implementation 'androidx.appcompat:appcompat:${versions.app-compat}'  

    ...
  
    // jetpack
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycle}"  
    implementation "androidx.lifecycle:lifecycle-extensions:${versions.lifecycle}"  
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.lifecycle}"  
 
    ...
  
}

To globally enable data binding between Views and ViewModels for the project, add this to the android section of build.gradle.

android {

    ...

    buildFeatures {  
        dataBinding = true  
    }
}

ViewModels

ViewModels provide the data a View (Activity or Fragment) intends to display, manipulate or based on its state render the UI. Jetpack provides a base-class for ViewModels which provides all mechanisms it needs to notify the UI in case data changes and manage its lifecycle: androidx.lifecycle.ViewModel

The reference implementation in the Android docs provides a practical base class, from which ViewModels in the app can inherit.

Layout Files

To enable Android resource layouts (XML view definitions) to support data binding to ViewModels their internal structure has to comply to the following format:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="viewModel" type="com.your.ModelType"/>
    </data>
    <androidx.coordinatorlayout.widget.CoordinatorLayout>
        ...
    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

The example above uses the androidx.coordinatorlayout:coordinatorlayout dependency, but this can be any layout suiting the View’s requirements.

Connect everything via Data Binding

To connect the View to the ViewModel there is some boilerplate setup code required. A part of it is best defined in a common base class for all views. Example for an Activity base class:

abstract class ActivityBase: AppCompatActivity() {
    protected inline fun <reified T : ViewDataBinding\> binding(@LayoutRes resId: Int): Lazy<T\> =  
        lazy { DataBindingUtil.setContentView<T>(this, resId) }  
}

This defines an inline function which can be used to obtain a reference of the binding in the View’s setup code.

In an Activity’s code behind, the binding is set up as follows:

class MainActivity : ActivityBase() {

    private val binding: ActivityMainBinding by binding(R.layout.activity_main)
    private val mainViewModel: MainViewModel by viewModels()

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

        binding.apply {
            lifecycleOwner = this@MainActivity
            viewModel = mainViewModel
        }
    }
    
}

The type ActivityMainBinding is generated by Jetpack when the referenced resource layout is defined in the format above (i.e. contains <data /> and layout). The name is aligned with the resource layout file and has the suffix Binding.

With this the view is bound to the view model and can react to changes to its data.

LiveData and Binding Expressions

The data exposed by ViewModels and consumed by Views might change while a View is visilbe. Hence there is a dedicated data type which enables Views to get notified of these changes: androidx.lifecycle.LiveData. “LiveData is an observable data holder class” (from LiveData Overview)

In the following example, an audio playback app, the ViewModel of an activity exposes a property of type MutableLiveData<Boolean>.

val isPlaying = MutableLiveData(false)

The ViewModel is connected to the view using the <variable /> tag mentioned above.

To update the View whenever its value changes, the respective control needs to bind to the property via a binding expression. The following does that to decide, which icon to render in an ImageView acting as play/pause button (see the android:src attribute).

<ImageView
    ...
    android:onClick="@{() -> viewModel.togglePlayPause()}"
    android:src="@{viewModel.isPlaying ? @drawable/icon_pause : @drawable/icon_pause}" />

Looking at the android:onClick attribute, shows how to interact with the ViewModel and mutate its state through user events triggered by the View, by directly accessing the viewModel variable.

RecyclerViewAdapter

In most apps, displaying collections of data (i.e. lists) is a necessity, so let’s have a look at how to implement data binding for a View using a androidx.recyclerview.widget.RecyclerView to display a list of items, defined as follows in the ViewModel.

val items = MutableLiveData<List<ItemModel>>()

First a layout for the ViewHolder is required, which is to be bound against the type of the item to display.

<layout ...>
    <data>  
  
    <variable name="item" type="com.your.ItemModel" />  
  
    </data>
    < ... View Layout with binding expressions ... />
</layout>

Furthermore an implementation of RecyclerView.Adapter<ViewHolder> is still required, however now it does not directly manipulate the controls in the View with the state of the ViewModel’s items, but sets up the binding between the two.

class ItemsAdapter(
    private val viewModel: ViewModel
) :
    RecyclerView.Adapter<ItemsAdapter.ItemViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        DataBindingUtil.inflate<ViewHolderItemBinding>(
            LayoutInflater.from(parent.context),
            R.layout.view_holder_item,
            parent,
            false
        ).let {
            ItemViewHolder(it)
        }

    override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
        holder.bind(viewModel.items.value!![position])
    }

    override fun getItemCount(): Int {
        if (viewModel.items.value == null) {
            return 0;
        }
        return viewModel.items.value!!.size
    }

    inner class ItemViewHolder(private val binding: ViewHolderItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(item: ItemModel) {
            binding.apply {
                item = item
                viewModel = viewModel
                executePendingBindings()
            }
        }

    }
}

Similar to the example before, Jetpack generates a binding data type based on the resource layout file’s name, in this case ViewHolderItemBinding. Generation happens on each build of the project. The adapter implementation consumes the items property of the viewModel, instantiates view holders and binds each view holder to the respective item.

In the View layout file, the RecyclerView must be bound against the adapter.

<data>
    <variable name="adapter" type="com.your.ItemsAdapter" />  
</data>

...

<androidx.recyclerview.widget.RecyclerView  
 bindAdapter="@{adapter}" />

In this case, this is done via bindAdapter, a custom BindingAdapter, which is explained in the next section.

BindingAdapters

In some occasions, the attributes in Android resource layout files do not suffice the need of what is to be bound, or which state of the UI needs to be changed based on a binding’s value. With androidx.databinding.BindingAdapter (BindingAdapter docs, Jetpack provides a mechanism to extend the standard set of bindable attributes, which can be freely implemented.

Some common examples are:

The bindAdapter implementation for RecyclerView and adapter, used above.

@BindingAdapter("bindAdapter")
fun bindAdapter(view: RecyclerView, adapter: RecyclerView.Adapter<*>?) {
    view.adapter = adapter;
}

Toggle a control’s visibility state based on Boolean property.

@BindingAdapter("goneUnless")
fun goneUnless(view: View, visible: Boolean) {
    view.visibility = if (visible) View.VISIBLE else View.GONE
}

Using an external library like Glide to load remote images, transform and display them

@BindingAdapter("bindImageUrl")
fun bindImageUrl(view: ImageView, url: String?) {

    if(url == null || url == "") {
        return;
    }

    Glide.with(view)
        .load(url)
        .error(R.drawable.error_icon)
        .placeholder(R.drawable.placeholder_icon)
        .apply(RequestOptions.bitmapTransform(RoundedCorners(16)))
        .into(view)
}

Custom binding adapters can be defined once in the app project and be reused in multiple layouts.

Sharing data across Views

There are scenarios, where it does make sense to share data from one ViewModel across multiple Views. A common example for this would be multiple Fragments hosted in a single Activity which rely on and manipulate similar state.

As a reminder, a View obtains the reference to its ViewModel by the viewModels inline function.

public val viewModel: FragmentViewModel by viewModels()

Assuming that this is done in a Fragment, that Fragment can obtain a reference to the ViewModel of its corresponding Activity via another inline function.

public val activityViewModel: ActivityViewModel by activityViewModels()

I found this mechanism particularly useful when implementing an application using androidx.navigation:navigation-fragment to display a tab navigation and on top of that, show a persistent Material Standard Bottom Sheet, which is defined as another Fragment in the activity. The BottomSheets state for being display/hidden or expanded/collapsed can then be defined in the main Activity’s ViewModel and accessed by all Fragments.

Feedback? I am happy to hear it. Feel free to reach out.