Android: Complete, generic data-binding RecyclerView adapter

0 min read (231 words)

Data binding greatly reduces the amount of code you need to connect user-interfaces with ViewModels. It keeps Activity and Fragment code small, and makes it easier to manage lifecycles.

<EditText
    android:id="@+id/username"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:text="@={viewModel.username}"/>

I discovered that there was no attribute to bind the elements in a RecyclerView, due to the fact that a RecyclerView needs an adapter to be able to create element views. It would also be nice to automatically use data binding to create the viewholders. There are a number of guides to do both of these halves, but I now present the code to do the whole.

Data binding greatly reduces the amount of code you need to connect user-interfaces with ViewModels. It keeps Activity and Fragment code small, and makes it easier to manage lifecycles.

<EditText
    android:id="@+id/username"
    android:layout_height="wrap_content"
    android:layout_width="match_parent"
    android:text="@={viewModel.username}"/>

I discovered that there was no attribute to bind the elements in a RecyclerView, due to the fact that a RecyclerView needs an adapter to be able to create element views. It would also be nice to automatically use data binding to create the viewholders. There are a number of guides to do both of these halves, but I now present the code to do the whole.

This guide assumes you have at least a beginner’s knowledge of data binding, and have it enabled for your project.

1. Generic data-binding RecyclerView adapter

The BindingRecyclerAdapter is a generic class which allows binding item holder views to the item ViewModel, and has a property to set the contained data. This property will be used by data binding later.

class BindingRecyclerAdapter<T, V>(val viewModel: V, @LayoutRes val layout: Int): RecyclerView.Adapter<BindingRecyclerAdapter.ViewHolder>() {
    var data: List<T> = emptyList()
        set(v) {
            field = v
            notifyDataSetChanged()
        }

    class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {}

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

    override fun getItemCount(): Int {
        return data.size
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.binding.setVariable(BR.item, data.getOrNull(position))
        holder.binding.setVariable(BR.viewModel, viewModel)
    }
}

2. Binding adapter

Now, we need to create a binding adapter to handle the data attribute:

@BindingAdapter("data")
fun <T> setRecyclerViewProperties(recyclerView: RecyclerView, data: List<T>?) {
    if (data == null) {
        return
    }

    if (recyclerView.adapter is BindingRecyclerAdapter<*, *>) {
        (recyclerView.adapter as BindingRecyclerAdapter<T, *>).data = data
    }
}

3. Parent View

First, you will need to add the ViewModel to the layout data section. Layouts with data-binding need to have <layout> as the outer-most view.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
                name="viewModel"
                type="com.example.app.viewmodels.MyListViewModel" />
    </data>

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

Next, let’s set up our RecyclerView to use the the new BindingRecyclerAdapter:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    binding.apply {
        myList.apply {
            setHasFixedSize(true)
            adapter = BindingRecyclerAdapter<Item>(R.layout.item)
            layoutManager = LinearLayoutManager(context)
        }

        // This is needed to subscribe to LiveData updates
        lifecycleOwner = this@MyListFragment
        viewModel = myListViewModel
        invalidateAll()
    }
}

4. Item Layout

<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>
        <import type="android.view.View" />
        <variable
                name="item"
                type="com.example.app.models.My" />
        <variable
                name="viewModel"
                type="com.example.app.viewmodels.MyListViewModel" />
    </data>

    <TextView
            android:id="@+id/user"
            style="?attr/titleTextAppearance"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:lines="1"
            android:layout_marginStart="16dp"
            android:text="@{item.author}"
            android:textStyle="bold"
            tools:text="Username" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Appendix A: Extending to add an Empty View

You may find it useful to add an empty view.

Add the following property to BindingRecyclerAdapter

class BindingRecyclerAdapter<T, V>(val viewModel: V, @LayoutRes val layout: Int): RecyclerView.Adapter<BindingRecyclerAdapter.ViewHolder>() {
    var emptyView: View? = null
        set(v) {
            field = v
            updateEmptyView()
        }

    var data: List<T> = emptyList()
        set(v) {
            field = v
            notifyDataSetChanged()

            updateEmptyView()
        }

    private fun updateEmptyView() {
        emptyView?.visibility = if (data.isEmpty()) View.VISIBLE else View.GONE
    }