
Работа со списками в Android проектах — это база. Большинство проектов использует RecyclerView из-за его гибкой настройки и переиспользования ViewHolder'ов. Но даже так существуют библиотеки, которые улучшают работу с RecyclerView.Adapter и RecyclerView.ViewHolder с более удобной компоновкой большого числа элементов списка.
Писать свою библиотеку это увлекательно и познавательно. Ты одновременно решаешь свои задачи и глубже познаёшь инструменты, которыми пользуешься. Всем советую.
Предлагаю вашему вниманию библиотеку, построенную на использование ViewBinding, которая решает мои задачи. Если не используете Compose, то скорее всего у вас включён viewBinding, плюсы которого уже много раз расписывали. Поэтому не буду на них останавливаться
Проблематика/хотелки:
Хочу один адаптер для всех списков, в экземпляры которого буду только передавать ViewHolder'ы
Калькуляции для обновления списка в фоновом потоке; маст хев для любой библиотеки
Не передавать отдельно каждый раз DiffUtil.ItemCallback
Улучшить работу с payload'ами
И вот он SingleRecyclerAdapter, единственный адаптер, экземпляры которого вы можете передавать в recycle вот так
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
with(recycler) {
adapter = binderAdapterOf(
HeaderUiModel::class bindWith HeaderViewHolderFactory(),
GroupUiModel::class bindWith GroupViewHolderFactory(
action = { title ->
Toast.makeText(context, "Clicked $title", Toast.LENGTH_SHORT).show()
}
)
)
setBindingList(dataFactory.createGroups())
}
}binderAdapterOf — функция создания ArrayMap
bindWith — используется для более удачных подсказок ide; эквивалент существующего инфикса to для создания Pair.
Как установить список?
Рассмотрим UiModel'и, это моделька реализующая интерфейс BindingClass.
Как видите, мы используем ui модели как кл��ч, чтобы потом получить ViewHolderFactory, которая создаст нам ViewHolder. Но не передаём колбек для обработки нашего списка.
А всё потому, что есть одна единственная реализация.
internal class BindingDiffUtilItemCallback : DiffUtil.ItemCallback<BindingClass>() {
override fun areItemsTheSame(oldItem: BindingClass, newItem: BindingClass): Boolean =
oldItem.areItemsTheSame(newItem)
override fun areContentsTheSame(
oldItem: BindingClass,
newItem: BindingClass
): Boolean = oldItem.areContentsTheSame(newItem)
override fun getChangePayload(
oldItem: BindingClass,
newItem: BindingClass
): Any = oldItem.getChangePayload(newItem)
}Колбек проксирует одноимённые методы из BindingClass. В результате, появляется возможность изменять логику проверок в самих модельках, реализующих интерфейс.
interface BindingClass {
val itemId: Long
get() = this.hashCode().toLong()
fun areContentsTheSame(other: BindingClass): Boolean = other == this
fun areItemsTheSame(other: BindingClass): Boolean = (other as? BindingClass)?.itemId == itemId
fun getChangePayload(newItem: BindingClass): List<Any> = listOf()
}А как вынести обновления списка в другой поток и зачем это нужно?
Это хорошая практика, особенно выручает, когда у вас очень сложный перегруженный экран, тогда потерять фреймы при отрисовке очень легко. От чего плавность работы экрана будет страдать. И пользователи будут расстраиваться, реже заходить в наше приложение, если не удалят его.
Ничего нового не придумал и использовал AsyncListDiffer, у которого есть метод submitList, который сам уведомит адаптер, когда закончит расчёты, выполняемые не в основном потоке.
class SingleRecyclerAdapter(
private val factory: ArrayMap<KClass<out BindingClass>, ViewHolderFactory<ViewBinding, BindingClass>>
) : RecyclerView.Adapter<BindingViewHolder<BindingClass, ViewBinding>>() {
private val items
get() = differ.currentList
private var differ: AsyncListDiffer<BindingClass> = AsyncListDiffer(
this@SingleRecyclerAdapter,
BindingDiffUtilItemCallback()
)
...
fun setItems(items: List<BindingClass>) = differ.submitList(items)
...Особенности работы с Payload
Полезная вещь, ведь мы не хотим каждый раз отрисовывать полностью элемент списка, когда происходит изменение только его части. Например, нажатие на кнопку лайка. Это выливается в лишнюю работу и неприятное мерцание, которое можно перебить лоадером на весь экран, что тоже не совсем красиво.
Поэтому в DiffUtil.ItemCallback есть третий метод getChangePayload , в котором мы конкретизируем изменения.
Если вы пишите обычный ViewHolder сами, то у вас есть перегрузки метода onBindViewHolder с payload и без. В моей библиотеке такого удовольствия не будет.
Плюсом такого решения является:
Нет необходимости метаться между двумя
функциямиНелишнее напоминание об присутствии payloads
Удобно, так как большая необходимость в использовании. В коммерческой разработке вам чаще нужна полезная нагрузка чем не нужна.
А вот и примеры:
Модель для отрисовки вложенного списка
data class GroupUiModel(
val title: String,
val items: List<InnerItemUiModel>
) : BindingClass {
// Как пример, решил, что если заголовок тот же самый
// значит используется та же самая группа
// поэтому использовал как itemId
// Чтобы при измение вложенного списка,
// у нас появлялось payload
override val itemId: Long = title.hashCode().toLong()
override fun getChangePayload(newItem: BindingClass): List<GroupPayload> {
val item = newItem as? GroupUiModel
// Функция создаёт список,
// фильтрую мапу по значенния, равным true
// возвращая ключи мапы
return checkChanges(
mapOf(
GroupPayload.ItemsChanged to (items != item?.items)
)
)
}
}
sealed class GroupPayload {
object ItemsChanged : GroupPayload()
}ViewHolderFactory с вложенным списком
class GroupViewHolderFactory(
private val action: (title: String) -> Unit
) : ViewHolderFactory<RecyclerItemGroupBinding, GroupUiModel> {
override fun create(
parent: ViewGroup
) = BindingViewHolder<GroupUiModel, RecyclerItemGroupBinding>(
RecyclerItemGroupBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
).apply {
with(binding) {
root.setOnClickListener {
action(item.title)
}
recycler.adapter = binderAdapterOf(
InnerItemUiModel::class bindWith InnerGroupViewHolderFactory()
)
}
}
override fun bind(
binding: RecyclerItemGroupBinding,
model: GroupUiModel,
payloads: List<Any>
) = when {
payloads.isNotEmpty() -> payloads.check { payload ->
when (payload) {
GroupPayload.ItemsChanged -> binding.recycler.setBindingList(model.items)
}
}
else -> with(binding) {
recycler.setBindingList(model.items)
title.text = model.title
}
}
}Функция расширения List<Any>.check на самом деле очень полезная и лично мне помогает расслабить мозг.
Дело в том, что если по какой-то причине произошло несколько быстрых обновлений элемента списка. То в bind может положиться несколько полезных нагрузок подряд(а может и не положится и будет второй вызов bind) и нужно их все не забыть обработать. В первом случае получаем один вызов bind с payloads == listOf(GroupPayload, GroupPayload)
а во втором два вызова bind, где список будет выглядеть как-то такpayloads == listOf(GroupPayload)
Ещё ситуация, вы передаёте в списке список полезных нагрузок (больше повторов богу повторов), например
payloads: List<Any> == listOf( listOf(Payload.Liked, Payload.AddToFavorites) )
И чтобы не задумываться каждый раз, что же приходит, нужно лишь вызвать функцию и написать обработчик payload через when. Это успех, это победа.
Конец
Вот ссылка на библиотеку, где можно посмотреть sample со списком в списке.
Всем спасибо за внимание и ознакомление с возможностями библиотеки.
Буду рад любым комментариям.