RecyclerView 2020: a modern way of dealing with lists in Android using DataBinding — Part 2

Anton Stulnev
7 min readMay 23, 2020

--

In the first article of this series, we discussed some disadvantages of the traditional “1 adapter per case” approach of working with RecyclerView in Android. I also demonstrated a simplified concept of a universal, DataBinding-based RecyclerView.Adapter which could serve as a potential solution to most of the discussed issues. In this post, I’d like to dive a little bit deeper into this topic and try to improve the original adapter by applying it to some real-world examples.

I also received a couple of interesting questions and I’m going to discuss the most popular ones as well. Thanks for your feedback on the Part 1 and welcome to the Part 2 :-)

Important: Please make sure you’ve already checked out Part 1 as I’m going to continue from where we stopped there. And here’s the library I created If you just want to play with this universal adapter in practice with minimal extra reading.

Let’s take our adapter from the previous post and try to apply it to a more dynamic case: this time we want to reload our screen every time it is resumed or reopened:

class MyViewModel : ViewModel() {
val data = MutableLiveData<List<RecyclerItem>()
// Called from Fragment's onStart()
fun loadData() {
SomeService.getUsers { users ->
data.value =
users.map { UserItemViewModel(it) }
.map { it.toRecyclerItem() }
}
}
}

private fun UserItemViewModel.toRecyclerItem() = RecyclerItem(
data = this,
variableId = BR.viewModel,
layoutId = R.layout.item_user
)

data class User(
val id: Int,
val firstName,
val lastname
)

class UserItemViewModel(val user: User) {
// Some logic
}

So we just update our LiveData every time the data is loaded and through the LiveData -> Fragment -> DataBinding -> BindingAdapter chain it will be delivered to our adapter’s updateData(list: List<RecyclerItem>) method. So far so good, but let’s check how this update operation looks visually for the case when we already had some data in the RecyclerView and our service returned the same result as it did the previous time:

Yack! RecyclerView didn’t have enough information to render the new list properly and we got this “blinking” effect as the result. But as you probably know, RecyclerView provides a lot of ways of dealing with such cases and one of them is DifUtil. In short, it allows you to define a set of comparator functions that can be used by RecyclerView.Adapter to determine whether some item changed its content (or position) and make a “to redraw / not to redraw” decision based on that. Now it’s time to integrate this extra feature into our adapter.

The simplest solution would be to extend the existing updateData(..) function to do something like this:

fun updateData(newItems: List<RecyclerItem>) {
val oldItems = this.data.copy()
val callback = object : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldItems.size
override fun getNewListSize(): Int = newItems.size

override fun areItemsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldItem: RecyclerItem = oldItems[oldItemPosition]
val newItem: RecyclerItem = newItems[newItemPosition]
// TODO: compare 2 items
}

override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldItem: RecyclerItem = oldItems[oldItemPosition]
val newItem: RecyclerItem = newItems[newItemPosition]
// TODO: compare 2 items
}
}
val diffResult = DiffUtil.calculateDiff(callback)
this.items.clear()
this.items.addAll(newItems)
diffResult.dispatchUpdatesTo(this)
}

This approach should do the trick but there is one caveat here: DiffUtil’s calculations may be relatively resource-consuming so the best practice is to do all the math in a background thread. Of course, we can do that manually, but fortunately, there is a better option — ListAdapter. It does exactly what we need automatically so let’s change our base class from RecyclerView.Adapter to ListAdapter as follows:

class DataBindingRecyclerAdapter : 
ListAdapter<RecyclerItem, BindingViewHolder>(DiffCallback()) {

override fun getItemViewType(position: Int): Int {
return getItem(position).layoutId
}

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding: ViewDataBinding = DataBindingUtil.inflate(inflater, viewType, parent, false)
return BindingViewHolder(binding)
}

override fun onBindViewHolder(
holder: BindingViewHolder,
position: Int
) {
getItem(position).bind(holder.binding)
holder.binding.executePendingBindings()
}
}

Two important details:

  • ListAdapter already contains a backing collection for its items so we no longer need to define mutableListOf<RecyclerItem> variable + we can get rid of the getItemCount(): Int override. Perfect! The less code is the better.
  • ListAdapter requires aDifUtil.ItemCallback constructor parameter.

The last thing we need to do is to implement this DifUtil.ItemCallback. While it’s a trivial task for simple adapters supporting single item types (here’s an example from the official Google samples), it may be not so easy for our universal adapter which operates by RecyclerItems whose data item can belong to any type. And what makes things even more complicated — we can have an unlimited amount of different types within a single adapter. So the only viable solution here is to provide the users of this adapter with the possibility to provide their own comparators for the types they’re going to use. Here’s the main interface we’re going to use for doing these comparisons:

interface RecyclerItemComparator {
fun isSameItem(other: Any): Boolean
fun isSameContent(other: Any): Boolean
}

As you’ve probably noticed, this interface is looking very similar to the ones provided by DiffUtil , the only difference is that it has only one input parameter instead of two. So it is something between Java’s equals and DiffUtil.ItemCallback. Now it’s time to implement the actual DiffCalback we’re already passing to DataBindingRecyclerAdapter's constructor:

private class DiffCallback : DiffUtil.ItemCallback<RecyclerViewItem>() {
override fun areItemsTheSame(
oldItem: RecyclerViewItem,
newItem: RecyclerViewItem
): Boolean {
val oldData = oldItem.data
val newData = newItem.data
// Use appropriate comparator's method if both items implement the interface
// and rely on plain 'equals' otherwise
return if (oldData is RecyclerItemComparator
&& newData is RecyclerItemComparator
) {
oldData.isSameItem(newData)
} else oldData == newData
}

override fun areContentsTheSame(
oldItem: RecyclerViewItem,
newItem: RecyclerViewItem
): Boolean {
val oldData = oldItem.data
val newData = newItem.data
return if (oldViewModel is RecyclerItemComparator
&& newViewModel is RecyclerItemComparator
) {
oldViewModel.isSameContent(newViewModel)
} else oldViewModel == newViewModel
}
}

That’s it. Our adapter will now try to compare RecyclerItems using the comparator we’ve just created and rely on simple equals otherwise. Now it’s time to implement RecyclerItemComparator in the UserItemViewModel:

class UserItemViewModel(val user: User) : RecyclerItemComparator {

override fun isSameItem(other: Any): Boolean {
if (this === other) return true
if (javaClass != other.javaClass) return false

other as UserItemViewModel
return this.user.id == other.user.id
}

override fun isSameContent(other: Any): Boolean {
other as UserItemViewModel
return this.user == other.user
}
}

The original “blinking” problem has finally gone and the adapter is now capable of handling more complicated cases like inserting a new item to the end of the previous list + position changes without extra redrawings:

Yay! Much better. And note that you don’t have to implement RecyclerItemComparator every time you use the adapter. This step is absolutely optional and can be skipped in many cases:

  • You don’t update the list very often and/or don’t care about possible blinkings
  • You use Kotlin’s Data classes inside RecyclerItems (== fallback in the DiffCallback)
  • You use classes that already override equals / hashCode properly (== fallback in the DiffCallback)

FAQ

Q: How do I handle clicks with this adapter? I can’t easily access my RecyclerView.ViewHolders.

A: DataBinding to the rescue. Again. First, define a new method in your data/item/view model:

class UserItemViewModel(val user: User) {
fun onClick() {
// handling logic
}
}

And now you just need to extend your item’s XML a little bit:

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

<data>
<variable
name="viewModel"
type="com.package.UserItemViewModel" />

</data>

<LinearLayout
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.onClick()}"
android:orientation="vertical">

<!-- Other content -->

</LinearLayout>

</layout>

What if you also need to deliver click events from list item entities to a parent or root ViewModel? There are many options available:

  • RxJava: use Subject + Observable pair to emit click events and observe them from the parent.
  • Coroutines: use Channel or Flow, everything else is the same as in the case of RxJava.
  • Callbacks: the simplest one, you can define a callback interface or lambda, create a mutable variable of this type in your item class and then assign a listener from the root view model.

Q: How do I update data in the adapter? What if I need to do a partial update, e.g. to update/replace a single element? Should I create new methods for that in the adapter or create its subclass?

A: The main and only way to update the content of the adapter is to call its updateData(list: List<RecyclerItem>) method (indirectly through the BindingAdapter from Part 1 post). This is by design. Manipulate by collections of your natural domain entities (like simple User POJO or item view models similar to UserItemViewModel) and then convert them to RecyclerItems right before exposing the final List<RecyclerItem> to your UI layer. Let the adapter and DiffUtil to do the rest, RecyclerView is smart enough to avoid unnecessary UI operations as long as you provide it with the information it needs.

Q: How can I access the data stored in List<RecyclerItem>? RecyclerItem's data parameter has Any type.

A: You don’t have to store two collections in your view models (like List<User> and List<RecyclerItem>) if you need access to the original data format you had before converting it into RecyclerItems, a list of RecyclerItems should suffice in most situations. A simple mapping like this one should be enough:

val data = mutableData<List<RecyclerItem>>()

fun doSomethingWithUsers() {
val users: List<User> =
data.value.orEmpty()
.map { it.data }
.filterIsInstance<User>()
// TODO process "users"
}

That’s basically all I was going to share with the community on this topic. Any feedback would be really appreciated and as I’ve already pointed out in the beginning, I prepared a simple library called DataBindingRecyclerAdapter so you can easily add it to your project and check in practice. I might extend it with extra features, documentation & samples in the future but for now, it is super simple and only contains the code listed in Part 1 & Part 2 posts.

Thanks for reading and good luck :-)

--

--