SingleRecyclerAdapter plus ViewBinding or why I wrote a library for lists


Working with lists in Android projects is the base. Most projects use RecyclerView due to its flexible configuration and reuse of ViewHolders. But even so, there are libraries that improve working with RecyclerView.Adapter And RecyclerView.ViewHolder with a more convenient arrangement of a large number of list items.

Writing your own library is fun and educational. You simultaneously solve your problems and get to know the tools you use more deeply. I advise everyone.

I bring to your attention a library built to use ViewBindingwhich solves my problems. If you are not using Compose, then most likely viewBinding is enabled, the advantages of which have already been described many times. So I won’t dwell on it.

Issues/Wishlist:

  1. I want one adapter for all lists, to whose instances I will only pass ViewHolders

  2. Calculations for updating the list in a background thread; must have for any library

  3. Do not pass separately each time DiffUtil.ItemCallback

  4. Improve work with payloads

And here he is SingleRecyclerAdapterthe only adapter you can pass instances to recycle like this

   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 creation function
bindWith – used for better ide hints; equivalent to an existing infix to to create Pair.

How to install a list?

Consider UiModel’i, this is a model that implements the BindingClass interface.
As you can see, we use the ui of the model as a key, so that we can then get a ViewHolderFactory, which will create a ViewHolder for us. But we do not pass a callback to process our list.

And all because there is only one implementation.

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)
}

The callback proxies the methods of the same name from the BindingClass. As a result, it becomes possible to change the logic of checks in the models themselves that implement the interface.

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()
}

But how to transfer list updates to another thread and why is this needed?

This is a good practice, especially when you have a very complex overloaded screen and losing frames when rendering is very easy. From which the smoothness of the screen will suffer. Users will get frustrated and less likely to visit our app if they don’t delete it.

Nothing new invented and used AsyncListDifferwhich has a submitList method that will notify the adapter itself when it has finished calculating.

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)
    ...

Features of working with Payload

A useful thing, because we do not want to draw the entire list element every time when only part of it changes. For example, squeezing a like. This results in unnecessary work and unpleasant flickering, which can be interrupted by the loader in full screen.
Therefore, in DiffUtil.ItemCallback there is a third method getChangePayload where we specify the changes.

If you write your own ViewHolder, then you have overloads of the onBindViewHolder method with and without payload. There will be no such pleasure in my library.

The advantage of this solution is:

  • There is no need to rush between the two
    functions

  • A useful reminder of the presence of payloads

  • Convenient, as a great need to use. In commercial development, you need a payload more often than you don’t.

And here are some examples:

Model for rendering a nested list

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 with nested list

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
        }
    }
}

Extension function List.check actually very useful and helps me personally to relax the brain.

The thing is, if for some reason there were several quick updates of the list item. Then bind can rely on several payloads in a row (or maybe not, and there will be a second bind call) and you need to remember to process them all.

Another situation, you convey in the list list of payloadsthat is

payloads: List<Any> == listOf( listOf(Payload.Liked, Payload.AddToFavorites) )

And in order not to think every time what comes, you just need to call the function and write the payload handler through when. This is success, this is victory.

End

Here link to the library, where you can see sample with the list in the list.

Thank you all for your attention and familiarization with the possibilities of the library.

I will be glad to any comments.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *