Работа со списками в 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 со списком в списке.

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

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

Only registered users can participate in poll. Log in, please.
Использовали ли бы эту библиотеку у себя в проекте?
20%Да1
80%Нет4
5 users voted. 2 users abstained.
Only registered users can participate in poll. Log in, please.
Пользуетесь ли вы библиотеками или пишите своё решение?
33.33%Используем стороннюю библиотеку2
33.33%Есть своё решение2
33.33%Ничего не используем2
6 users voted. 1 user abstained.