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.