SingleRecyclerAdapter плюс ViewBinding или зачем я написал библиотеку для списков

от автора

Работа со списками в Android проектах — это база. Большинство проектов использует RecyclerView из-за его гибкой настройки и переиспользования ViewHolder’ов. Но даже так существуют библиотеки, которые улучшают работу с RecyclerView.Adapter и RecyclerView.ViewHolder с более удобной компоновкой большого числа элементов списка.

Писать свою библиотеку это увлекательно и познавательно. Ты одновременно решаешь свои задачи и глубже познаёшь инструменты, которыми пользуешься. Всем советую.

Предлагаю вашему вниманию библиотеку, построенную на использование ViewBinding, которая решает мои задачи. Если не используете Compose, то скорее всего у вас включён viewBinding, плюсы которого уже много раз расписывали. Поэтому не буду на них останавливаться

Проблематика/хотелки:

  1. Хочу один адаптер для всех списков, в экземпляры которого буду только передавать ViewHolder’ы

  2. Калькуляции для обновления списка в фоновом потоке; маст хев для любой библиотеки

  3. Не передавать отдельно каждый раз DiffUtil.ItemCallback

  4. Улучшить работу с 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 со списком в списке.

Всем спасибо за внимание и ознакомление с возможностями библиотеки.

Буду рад любым комментариям.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Использовали ли бы эту библиотеку у себя в проекте?

20% Да1
80% Нет4

Проголосовали 5 пользователей. Воздержались 2 пользователя.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Пользуетесь ли вы библиотеками или пишите своё решение?

33.33% Используем стороннюю библиотеку2
33.33% Есть своё решение2
33.33% Ничего не используем2

Проголосовали 6 пользователей. Воздержался 1 пользователь.

ссылка на оригинал статьи https://habr.com/ru/articles/730218/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *