Skip to main content

Android: Complete, generic data-binding RecyclerView adapter

Sidebar

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. ```xml ``` 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
    }
rubenwardy's profile picture, the letter R

Hi, I'm Andrew Ward. I'm a software developer, an open source maintainer, and a graduate from the University of Bristol. I’m a core developer for Luanti, an open source voxel game engine.

Comments

Leave comment

Shown publicly next to your comment. Leave blank to show as "Anonymous".
Optional, to notify you if rubenwardy replies. Not shown publicly.
Max 1800 characters. You may use plain text, HTML, or Markdown.