Hello. The other day I ran into the problem of implementing a selection of multiple items in the RecyclerView using dataBinding.
Get started
First, let's write a basic adapter that supports dataBinding.
/**
* data binding'.
* @param layoutRes id layout',
* @param lifecycleOwner lifecycle owner , recycler view
* @param itemBindingId id layout',
* @param onClick ,
*/
class RecyclerViewAdapter<Item : IRecyclerViewItem>(
@LayoutRes private val layoutRes: Int,
private val lifecycleOwner: LifecycleOwner,
private val itemBindingId: Int? = null,
private val onClick: ((Item) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerViewAdapter<Item>.ViewHolder>() {
private val items = mutableListOf<Item>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
// ViewDataBinding layoutRes
val binding = DataBindingUtil.inflate<ViewDataBinding>(inflater, layoutRes, parent, false)
return ViewHolder(binding)
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
// onBind
val item = items[position]
holder.onBind(item)
}
/**
*
*
* @param newItems
*/
fun setItems(newItems: List<Item>) {
val diffUtilCallback = DiffUtilCallback(newItems)
val diffResult = DiffUtil.calculateDiff(diffUtilCallback)
items.apply {
clear()
addAll(newItems)
}
diffResult.dispatchUpdatesTo(this)
}
// DataBinding'
inner class ViewHolder(
private val binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(item: Item) {
binding.apply {
//
setVariable(itemBindingId ?: BR.item, item)
root.setOnClickListener { onClick?.invoke(item) }
lifecycleOwner = this@RecyclerViewAdapter.lifecycleOwner
}
}
}
private inner class DiffUtilCallback(private val newItems: List<Item>) : DiffUtil.Callback() {
override fun getOldListSize(): Int = itemCount
override fun getNewListSize(): Int = newItems.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newItems[newItemPosition].id == items[oldItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newItems[newItemPosition] == items[oldItemPosition]
}
}
}
Also, for DiffUtil to work, I made an interface that shows that the element has a unique field
/**
* ui , RecyclerViewAdapter.
* @property id
*/
interface IRecyclerViewItem {
val id: Int
}
, . onClick onBind: (binding: ViewDataBinding) -> Unit, .
Selection helper
, SelectionHelper, dataBinding' .
, , , .
, , :
class SelectionHelper<T : IRecyclerViewItem> : ISelectionHelper<T>() {
//
private val selectedItems = mutableMapOf<Int, T>()
// , - , -
override fun handleItem(item: T) {
if (selectedItems[item.id] == null) {
selectedItems[item.id] = item
} else {
selectedItems.remove(item.id)
}
// dataBining, ui)
notifyChange()
}
override fun isSelected(id: Int): Boolean = selectedItems.containsKey(id)
override fun getSelectedItems(): List<T> = selectedItems.values.toList()
override fun getSelectedItemsSize(): Int = selectedItems.size
}
// BaseObservable, , dataBinding
//
abstract class ISelectionHelper<T : IRecyclerViewItem> : BaseObservable() {
abstract fun handleItem(item: T)
abstract fun isSelected(id: Int): Boolean
abstract fun getSelectedItems(): List<T>
abstract fun getSelectedItemsSize(): Int
}
:
viewModel selectionHelper.getSelectedItems, .
DataBinding, - adapter
, onBind
:
viewModel/presenter ,
xml
- ,
adadpter
class RecyclerViewAdapter<Item : IRecyclerViewItem>(
@LayoutRes private val layoutRes: Int,
private val lifecycleOwner: LifecycleOwner,
private val itemBindingId: Int? = null,
// , ,
private val selectionHelper: ISelectionHelper<Item>? = null,
private val onClick: ((Item) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerViewAdapter<Item>.ViewHolder>() {
...
inner class ViewHolder(
private val binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(item: Item) {
binding.apply {
//
setVariable(itemBindingId ?: BR.item, item)
selectionHelper?.let { setVariable(BR.selectionHelper, it) }
root.setOnClickListener {
//
selectionHelper?.handleItem(item)
onClick?.invoke(item)
}
lifecycleOwner = this@RecyclerViewAdapter.lifecycleOwner
}
}
}
}
/ , , onBind, - .
class RecyclerViewAdapter<Item : IRecyclerViewItem>(
@LayoutRes private val layoutRes: Int,
private val lifecycleOwner: LifecycleOwner,
private val itemBindingId: Int? = null,
private val selectionHelper: ISelectionHelper<Item>? = null,
private val onClick: ((Item) -> Unit)? = null
) : RecyclerView.Adapter<RecyclerViewAdapter<Item>.ViewHolder>() {
private val items = mutableListOf<Item>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = DataBindingUtil.inflate<ViewDataBinding>(inflater, layoutRes, parent, false)
return ViewHolder(binding)
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.onBind(item)
}
/**
*
*
* @param newItems
*/
fun setItems(newItems: List<Item>) {
val diffUtilCallback = DiffUtilCallback(newItems)
val diffResult = DiffUtil.calculateDiff(diffUtilCallback)
items.apply {
clear()
addAll(newItems)
}
diffResult.dispatchUpdatesTo(this)
}
inner class ViewHolder(
private val binding: ViewDataBinding
) : RecyclerView.ViewHolder(binding.root) {
fun onBind(item: Item) {
binding.apply {
//
setVariable(itemBindingId ?: BR.item, item)
selectionHelper?.let { setVariable(BR.selectionHelper, it) }
root.setOnClickListener {
//
selectionHelper?.handleItem(item)
onClick?.invoke(item)
}
lifecycleOwner = this@RecyclerViewAdapter.lifecycleOwner
}
}
}
private inner class DiffUtilCallback(private val newItems: List<Item>) : DiffUtil.Callback() {
override fun getOldListSize(): Int = itemCount
override fun getNewListSize(): Int = newItems.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newItems[newItemPosition].id == items[oldItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newItems[newItemPosition] == items[oldItemPosition]
}
}
}
, xml , selectionHelper xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="ImageItemUi" />
<!-- ) -->
<variable
name="selectionHelper"
type="dev.syncended.ctime.utils.ui.ISelectionHelper<ImageItemUi>" />
<import type="dev.syncended.ctime.models.ui.ImageItemUi" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="@dimen/ui_spacing_normal"
android:padding="@dimen/1dp"
android:scaleType="centerCrop"
app:file="@{item.file}"
app:item_id="@{item.id}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:selection_helper="@{selectionHelper}"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
padding = 1dp, , , .
bindingAdapter selectionHelper
// selectionHelper', id
@BindingAdapter("selection_helper", "item_id", requireAll = true)
fun <T : IRecyclerViewItem> handleSelection(
view: View,
selectionHelper: ISelectionHelper<T>,
itemId: Int
) {
//
val isSelected = selectionHelper.isSelected(itemId)
//
val color = if (isSelected) {
R.color.color_primary
} else {
android.R.color.transparent
}
view.setBackgroundColor(ContextCompat.getColor(view.context, color))
}
, background.
:
:
. , , , , , .
android:checked=@{selectionHelper.isSelected(item.id)}
.
, , , .