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

Anton Stulnev
8 min readMay 11, 2020

--

RecyclerView is undoubtedly one of the most important and widely used UI widgets available in Android SDK nowadays, and it’s really hard to imagine a modern application with no lists at all. There are hundreds of articles about this component and there is a lot of great libraries simplifying its usage provided by the community. Nevertheless, I’d like to add my 5 cents here and discuss traditional approaches of working with lists, what problems they bring, and how they can potentially be solved by using Android Data Binding Library.

Let’s take a look at a canonical implementation of aRecyclerView.Adapter, shamelessly borrowed from the official docs:

class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

class MyViewHolder(
val textView: TextView
) : RecyclerView.ViewHolder(textView)

private var data: List<String> = emptyList()

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val textView = inflater.inflate(R.layout.my_text_view, parent, false) as TextView
return MyViewHolder(textView)
}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.textView.text = data[position]
}

override fun getItemCount() = myDataset.size

fun updateData(list: List<String>) {
data = list
notifyDataSetChanged()
}
}

Quite a lot of code for such a simple task as rendering a list of strings on screen, isn’t it? But what’s even more important, this adapter is tightly coupled to a single data type (String) and XML layout. Essentially, we often have a 1 : 1 mapping between the number of lists in the app and the number of RecyclerView.Adapter implementations: we have to create a new adapter (including RecyclerView.ViewHolder) pretty much every time we’re dealing with a new list. What’s even worse, a significant part of these implementations consists of identical boilerplate code which is only there to match the contract of the RecyclerView.Adapter abstract class. Pretty annoying I’d say. But let’s continue with this adapter anyway and try to link it to a hypothetical screen based on Fragment or Activity.

Luckily, the days when writing all the business logic right inside Activities and Fragments was a common practice are gone and a modern Android developer would extract it into a separate entity like Presenter or ViewModel:

class MyViewModel : ViewModel() {

val data = MutableLiveData<MyObject>()

init {
loadData()
}

private fun loadData() {
// ...fetch data from API / DB and put it to `data`
data.value = ...
}
}

View models & presenters do not know anything about RecyclerViews, hence they can’t update the UI directly. Instead, we need to write some extra code in the UI layer to complete the chain:

class MyFragment : Fragment() {

val viewModel: MyViewModel by viewModels()

private val adapter = MyAdapter()
...
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
viewModel.data.observe(viewLifecycleOwner, Observer {
adapter.updateData(it)
})
}
}

Quite a typical code and I bet most of us have already seen or even written something very similar many times before. What problems does it have? First of all, we have to create an adapter manually and store a reference to it so it can be updated if the data set changes. Second, we have to set up observing the data exposed by View Model and react to these changes by updating the underlying adapter’s data set. While both these issues do not bring any architectural or ideological issues, they still lead to writing extra boilerplate code in our UI layer, we have to manually link the two worlds — plain & natural data flowing in the business logic layer (View Models, Repositories, etc.) and UI handled by Views, Fragments or Activities.

Well, we’ve found the following areas for improvement in the “classic” way of working with Lists & Adapters in Android:

  1. We have to create new & new adapters for almost every single individual RecyclerView in the project. These adapters can’t always be reused and contain a lot of boilerplate code.
  2. It would be a bad idea to store RecyclerView's adapters right inside view models for lots of reasons (separation of concerns, view models reusability) so we have to expose domain-level data from View Models / Presenters and manually convert it to its UI representation by using adapters. This leads to writing extra code in Fragments and Activities + UI layer becomes the one that performs actual mapping, we expose domain (POJOs, Data Classes, etc.) format to the UI layer so it can do the conversion.

As some of you probably know, there is a solution to both of these problems and it’s called Android Data Binding framework. Let’s get started…!

To begin with, let’s create a new data class which will be the only format used by our future universal Adapter:

  • data will hold… your data. It can also be treated as a “view model of individual list item”. It can either be something as simple as a plain POJO or something more complex with actual business logic, our adapter will not care about that and it is up to you to decide depending on the use case.
  • layoutId — this is the XML file containing the layout for rows of the list. This is where our adapter will try to find variableId to initialize it with the value of data. Essentially, this is a reference to a layout that will be used to render the data.
  • variableId contains the name of the variable which will be used in your XML layout inside <data></data> tag. We’ll discuss this in more detail below. Also, check out the Data Binding : Dynamic Variables docs for more details.

Now it’s time to implement the universal DataBinding adapter itself:

That’s basically it, the MVP version of our universal adapter is ready. Let’s quickly discuss what’s going on here.

  • getItemViewType: as you probably know, this callback is typically used to support multiple view types within single RecyclerView & adapter. We simply return layoutId from our RecyclerItem and this trivial action allows our adapter to automatically handle different view types. We just use the generated XML layout’s ID as a “view type” identifier.
  • onCreateViewHolder: most of the data-binding magic happens here. We use DataBindingUtil to dynamically create a new ViewDataBinding based on XML layout ID. All data-binding enabled XMLs have a generated class derived from this base class. We don’t know the name of this class here but we know that it will be generated for the given layoutId and it will inherit ViewDataBinding.
  • onBindViewHolder: here we need to link our ViewDataBinding with viewModel. Every data binding class has theserVariable() method which accepts variable ID and data of Object type. As you might have guessed, these are our variableId and viewModel from the RecyclerItem.

As we mentioned earlier, we’d like to avoid writing any glue code in our Fragments and Activities so the last step is to create a BindingAdapter which can help us to do everything with just 1 line of code in Fragment or Activity XML:

Now it’s time to see how all of that works together in practice. Let’s say we need to implement a screen displaying a list of Users. Here’s how your POJO class may look like:

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

Now let’s create a layout that will be used for individual rows:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

<data>
<variable
name="user"
type="com.package.User" />

</data>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.firstName}" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{user.lastName}" />

</LinearLayout>

</layout>

Nothing new here for those who are familiar with the Data Binding framework. Now back to our view model: we still need to fetch Users and then “put” them into a RecyclerView somehow.

class MyViewModel : ViewModel() {

val data = MutableLiveData<List<RecyclerItem>()

init {
loadData()
}

private fun loadData() {
SomeService.getUsers { users ->
data.value = users.map { it.toRecyclerItem() }
}
}
}

As you see, now we have List<RecyclerItem> instead of something like List<User> and we use user.toRecyclerItem() method to convert our data classes into something which can be recognized by RecyclerViewRecyclerItem . We don’t want to spoil our POJOs with logic that belongs to another layer of abstraction (UI) so let’s instead create a private extension in the file holding the ViewModel:

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

That’s the only “glue” code we need to write in order to bridge the gap between ViewModel’s natural data format (list of Users) and RecyclerView. The last step we need to do is to render the List<RecyclerItem> in our UI. Thanks to DataBinding and the BindingAdapter we created earlier, all of that can be done in XML:

<?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.package.MyViewModel" />

</data>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:items="@{viewModel.data}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</layout>

The final step would be to assign your actual ViewModel to the viewModel variable from above:

class MyFragment : Fragment() {

val viewModel: MyViewModel by viewModels()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FragmentMyScreenBinding.inflate(inflater, container, false)
.also {
it.viewModel = viewModel
it.lifecycleOwner = viewLifecycleOwner
}
.root
}
}

That’s it and that’s the only code you need to write in your Activities or Fragments when you use DataBinding and the “adapterless” approach we’re discussing in this article. Pretty nice, huh? :-)

Let’s revisit what we’ve done so far to check whether it is any better than the original version with “1 adapter for 1 list”.

  • We no longer need to create new adapters. Ever. With minor additions, thisRecyclerViewAdapter can be the only adapter in your project.
  • Our Fragments and Activities no longer need to worry about adapters and data observing. The only thing they need to do is to assign a viewModel to the corresponding XML layout and this looks & feels pretty natural. Everything else is handled in XML with just a couple of lines of code.
  • We no longer need to expose domain data format from View Models to Activities and Fragments. Instead, you just need to provide a generic list of RecyclerItems which hides the actual data format from the UI and RecyclerItem can be handled by the UI directly with no extra transformations (thanks to the DataBindingAdapter and RecyclerViewAdapterit uses under the hood).
  • You don’t need to spoil your POJOs or recycler item View Models by extending some base class, you don’t even need to implement any extra interface. The POJOs or view models you use for RecyclerView look like regular classes. You just need to write a mapper somewhere where it makes the most sense for you (static or extension function returning a RecyclerItem).
  • There is no need to extend the adapter every time when a new item type is added. You can easily support as many view types in a single list as you want, you just need to define mappings from your data to RecyclerItem:
val data = MutableLiveData<List<RecyclerItem>()

fun loadData() {
val users: List<User> = ...fetch users...
val admins: List<Admin> = ...fetch admins...
data.value =
users.map { it.toRecyclerItem()) }
+ admins.map { it.toRecyclerItem() }
}

fun User.toRecyclerItem() = RecyclerItem(
data = this,
variableId = BR.user,
layoutId = R.layout.item_user
)

fun Admin.toRecyclerItem() = RecyclerItem(
data = this,
variableId = BR.admin,
layoutId = R.layout.item_admin
)

Much better than the new MergeAdapter, isn’t it? :-)

What’s next? As I mentioned earlier, the current implementation is a bit simplified: it will work just fine for relatively static cases with rare updates of the content. But for more dynamic scenarios it would be great to also integrate DifUtiland move its calculations to a background thread. I’ll try to uncover this topic (along with some other improvements) in the next article.

Thanks for reading and please let me know what you think about this approach ;-)

UPD: Part 2 of this series can be found here

--

--