Всем привет. На днях столкнулся с проблемой реализации выбора нескольких элементов в RecyclerView с использованием dataBinding'а.
Сразу за дело
Для начала напишем базовый адаптер, поддерживающий 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]
}
}
}Так же для работы DiffUtil я сделал интерфейс, который показывает, что элемент имеет уникальное поле
/**
* Интерфейс для 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)}По аналогии можно сделать кучу разных вариаций использования данного хэлепера.
Спасибо за прочтение, это моя первая статья, так что не судите строго, а так же держите котика.
