Android: Complete, generic data-binding RecyclerView adapter
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
}
Comments