Blog Infos
Author
Published
Topics
, ,
Published

Three years ago I wrote an article about binding a list of items to a RecyclerView that became quite popular. It’s not really a surprise, since creating lists is one of the most common use cases in Android development.

A lot of things happened since then. Most importantly we got an official architecture guideline from Google with the Android Architecture Components. Also much more projects are using Data Binding nowadays.

I thought it’s time to revisit this topic and see how we can achieve something similar using the AAC ViewModel, but this time we will go a little further. We will create a RecyclerView Adapter that can be used to bind multiple types of data in the same list.

To showcase how can we bind data with multiple view types, we will create the following sample app:

The list has three types of items: headers, car listings, and some promoted listings that stand out from the rest.

We start with implementing the ViewModel. This will be really simple in our case. We just get the data from our data provider asynchronously, do some transformations on the raw data to make it suitable for our UI, then set the value of our LiveData object which will be bound to the XML layout.

If you are not familiar with the AAC ViewModel I suggest reading about it first in the official Google documentation.

Note: I used Hilt for dependency injection and Coroutines for background processing in this sample project, but those are absolutely not necessary to understand and implement this.

@HiltViewModel
class CarListViewModel @Inject constructor(
private val carDataProvider: CarDataProvider
) : ViewModel() {
val data: LiveData<List<ItemViewModel>>
get() = _data
private val _data = MutableLiveData<List<ItemViewModel>>(emptyList())
init {
loadData()
}
private fun loadData() {
// This is a coroutine scope with the lifecycle of the ViewModel
viewModelScope.launch {
// getCarListData() is a suspend function
val carList = carDataProvider.getCarListData()
val carsByMake = carList.groupBy { it.make }
val viewData = createViewData(carsByMake)
_data.postValue(viewData)
}
}
private fun createViewData(carsByMake: Map<String, List<CarData>>): List<ItemViewModel> {
val viewData = mutableListOf<ItemViewModel>()
carsByMake.keys.forEach {
viewData.add(HeaderViewModel(it))
val cars = carsByMake[it]
cars?.forEach { car: CarData ->
val item = if (car.isAd) {
CarAdViewModel(car.make, car.model, car.price)
} else {
CarListingViewModel(car.make, car.model, car.price)
}
viewData.add(item)
}
}
return viewData
}
}

The most important thing from the above code is the data property which will hold all the data what we need for our UI. This data will contain a list of ItemViewModel instances.

The ItemViewModel in the above code is an interface which represents the ViewModel part of the MVVM architecture on a list item level. These are not subclasses of the ViewModel class from AAC, but can also be observable (we will get back to this later).

You can see in the createViewData function that we add different objects (HeaderViewModelCarAdViewModelCarListingViewModel) to the list, depending on what we want to show on the UI. These are all implementations of the ItemViewModel interface, and represent the individual items in the list.

Let’s see how it looks like in code, it’s much easier to understand that way:

interface ItemViewModel {
@get:LayoutRes
val layoutId: Int
val viewType: Int
get() = 0
}
class HeaderViewModel(val title: String) : ItemViewModel {
override val layoutId: Int = R.layout.item_header
override val viewType: Int = CarListViewModel.HEADER_ITEM
}
class CarAdViewModel(val make: String, val model: String, val price: String) : ItemViewModel {
override val layoutId: Int = R.layout.item_car_listing_ad
override val viewType: Int = CarListViewModel.AD_ITEM
}
class CarListingViewModel(val make: String, val model: String, val price: String) : ItemViewModel {
override val layoutId: Int = R.layout.item_car_listing
override val viewType: Int = CarListViewModel.LISTING_ITEM
}

All the ItemViewModel implementations will specify their layoutId what will be inflated when we need to show that item. They all have different properties that can be referenced in the layout files of the items to bind the data. For example here is the layout file of the HeaderViewModel to see how the properties are bound to the view:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="itemViewModel"
type="com.tamaskozmer.flexiblerecyclerview.itemviewmodels.HeaderViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="14sp"
android:layout_marginBottom="4dp"
android:text="@{itemViewModel.title}"
android:textStyle="bold"
tools:text="Toyota" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/darker_gray" />
</LinearLayout>
</layout>
view raw item_header.xml hosted with ❤ by GitHub

Finally we also need to define the viewType property for each ItemViewModel, because the RecyclerView need to know the view type of its elements. These can be simple constants.

To connect the list of ItemViewModels in the CarListViewModel we will need to create two things. A custom RecyclerView.Adapter implementation and a Binding Adapter. We only need to create these once, then we can reuse it for every screen, where we need to show a list of items.

BindableRecyclerViewAdapter

Let’s start with the RecyclerView.Adapter implementation, it’s the more complicated of the two, but we will go through all of the important callbacks step by step.

class BindableRecyclerViewAdapter : RecyclerView.Adapter<BindableViewHolder>() {
var itemViewModels: List<ItemViewModel> = emptyList()
private val viewTypeToLayoutId: MutableMap<Int, Int> = mutableMapOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindableViewHolder {
val binding: ViewDataBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
viewTypeToLayoutId[viewType] ?: 0,
parent,
false)
return BindableViewHolder(binding)
}
override fun getItemViewType(position: Int): Int {
val item = itemViewModels[position]
if (!viewTypeToLayoutId.containsKey(item.viewType)) {
viewTypeToLayoutId[item.viewType] = item.layoutId
}
return item.viewType
}
override fun getItemCount(): Int = itemViewModels.size
override fun onBindViewHolder(holder: BindableViewHolder, position: Int) {
holder.bind(itemViewModels[position])
}
fun updateItems(items: List<ItemViewModel>?) {
itemViewModels = items ?: emptyList()
notifyDataSetChanged()
}
}
class BindableViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(itemViewModel: ItemViewModel) {
binding.setVariable(BR.itemViewModel, itemViewModel)
}
}
So the adapter has two properties:
  • The itemViewModels represents the content of the list.
  • The viewTypeToLayoutId is a map that associates the possible view types of this adapter with the layout id of that specific view type. This is important, because when we create the ViewHolder we only know the view type, but we also need to know what layout to inflate for that ViewHolder.

And the callbacks what we need to implement are the following:

  • onCreateViewHolder – We use the DataBindingUtil here to inflate our layout, because we are using Data Binding in the items as well. We inflate the layout into a ViewDataBinding object and create the BindableViewHolder with it. Later we can set variables to this binding, but this method is only responsible for creating the ViewHolder. We use the viewTypeToLayoutId here to get the correct layout id.
  • onBindViewHolder – This is where we can set the variables for the binding, in this case only the itemViewModel itself.
  • getItemViewType – This is a very important part of the implementation. Here we can use the position parameter to get the item and associate its layout id with its view type.

There is also the updateItems method, what we will call from the Binding Adapter. This will just update the items and call notifyDataSetChanged. If you want to implement a more sophisticated version with animations, you can use DiffUtil and notify each item change separately.

Binding Adapter
@BindingAdapter("itemViewModels")
fun bindItemViewModels(recyclerView: RecyclerView, itemViewModels: List<ItemViewModel>?) {
val adapter = getOrCreateAdapter(recyclerView)
adapter.updateItems(itemViewModels)
}
private fun getOrCreateAdapter(recyclerView: RecyclerView): BindableRecyclerViewAdapter {
return if (recyclerView.adapter != null && recyclerView.adapter is BindableRecyclerViewAdapter) {
recyclerView.adapter as BindableRecyclerViewAdapter
} else {
val bindableRecyclerAdapter = BindableRecyclerViewAdapter()
recyclerView.adapter = bindableRecyclerAdapter
bindableRecyclerAdapter
}
}

If you are familiar what are Binding Adapters, this looks really simple. If the RecyclerView doesn’t have an adapter yet, we create a new one and assign it to the RecyclerView. After that we call the updateItems method on the adapter with our data.

This is how it looks like in the XML of the MainActivity.

<?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">
<data>
<variable
name="viewModel"
type="com.tamaskozmer.flexiblerecyclerview.CarListViewModel" />
</data>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:itemViewModels="@{viewModel.data}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</layout>

And finally the code of the MainActivity where we inflate the binding and set the viewModel property.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: CarListViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
binding.viewModel = viewModel
setContentView(binding.root)
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

That’s it. We created a flexible custom RecyclerView Adapter that can be used on any screen with list content and we don’t need to create RecyclerView Adapters and ViewHolders anymore.

And the best thing is that this makes our code easily testable, since all the logic regarding the content of the list will be in the ViewModel. If we have any complex logic in the items, they can be also tested separately.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

While this code works great, it’s simple and reusable, there is always room for improvement. In most cases we won’t need these, but if code becomes more complex we might benefit from it.

Making an ItemViewModel observable

We sometimes have items that can react to specific events and change their appearance. Let’s take the previous promoted car listing item with the red border for example.

When the user clicks the price of the car, we want to change the color of the border. If we wanted to achieve this with the current implementation, we would need to create a new itemViewModel at that position and update the list. This is far from ideal.

If we make the CarAdItemViewModel observable by subclassing it from the BaseObservable class we can change the borderColor property when the price is clicked, notify that the property is changed and the UI will update.

class CarAdViewModel(
val make: String,
val model: String,
val price: String,
@get:Bindable var borderColor: Int
) : BaseObservable(), ItemViewModel {
override val layoutId: Int = R.layout.item_car_listing_ad
override val viewType: Int = CarListViewModel.AD_ITEM
fun onCarPriceClick() {
borderColor = getRandomColor()
notifyPropertyChanged(BR.borderColor)
}
}
Binding any property to the item layouts

In the previous example we are only binding the itemViewModel property to the inflated layout in the BindableViewHolder. In this case we will access the itemViewModel in the XML.

If we want to make this more flexible by defining what properties we want to bind, we can modify our code like this:

interface ItemViewModel {
...
val bindableProperties: Map<Int, Any>
}
class CarAdViewModel(val make: String, val model: String, val price: String) : ItemViewModel {
...
override val bindableProperties: Map<Int, Any> = mapOf(
BR.make to make,
BR.model to model
)
}
class BindableViewHolder(private val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(itemViewModel: ItemViewModel) {
itemViewModel.bindableProperties.entries.forEach {
binding.setVariable(it.key, it.value)
}
}
}

Thanks for reading my article. I hope you found it useful, and it can help you make your users happier.

If you liked it share it with fellow Android developers. If you have any questions or suggestions feel free to leave a comment.

The complete code of this sample project can be found in the following Github repository:

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Jetpack Compose uses a pattern called state hoisting to make stateless composables and move…
READ MORE
blog
As an Android developer, I am sure you have come across the term MVVM…
READ MORE
blog
Recently after starting to migrate to Jetpack Compose, we decided to migrate the CLEAN…
READ MORE

Leave a Reply

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

Fill out this field
Fill out this field
Please enter a valid email address.

Menu