![](https://habrastorage.org/getpro/habr/upload_files/c22/8a0/337/c228a0337a7002a464ba3ea8837221e3.jpg)
Работа со списками в 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 со списком в списке.
Всем спасибо за внимание и ознакомление с возможностями библиотеки.
Буду рад любым комментариям.
ссылка на оригинал статьи https://habr.com/ru/articles/730218/
Добавить комментарий