Headache from RecyclerView.Adapter - there is a way out

    Hello, Habr! Today on our blog is Max Tuev, architect of Surf , one of our certified studios. The guys are engaged in custom development, so the timing is no less important than the quality of the code. The approaches and technologies that impede development are not suitable here. A good example of this is RecyclerView.Adapter. Under the cut, Max will tell you how to save time and nerves. Word to Max.



    RecyclerView.Adapter handles simple lists with a bang. But an attempt to implement an adapter for complex cases with several types of cells sometimes leads to the birth of monsters, which developers try to forget as quickly as possible and not touch them anymore. But it happens that a new update brings a couple more cells to this hardly alive adapter. If that sounds familiar, welcome to Cat. I’ll tell you how we in the studio solved these problems. In particular, I’ll show you how to learn how to manage a list using only 10 lines of code. An example is on the GIF below.


    Where do monster legs grow from? The main problems are created by 2 adapter features:

    1. Controlling the position of elements (getItemViewType, onBindViewHolder, ...)
    2. Determining which elements to call the notifyItem methods for ...

    Let's start with the second point


    This problem can be solved using notifyDataSetChanged (). But then it will not work to use ItemAnimator, which was unacceptable for us.

    DiffUtil, which recently appeared in the Support Library, helped to solve this. This class allows you to determine what has changed in the list and to notify the adapter about changes. To do this, transfer the old and new lists to it. Here is a good example .

    The problem with notify would seem to be resolved. But it’s not so simple. For example, if we change one field of an object from the list, then DiffUtil will not work. And such a code is very common. If you look closely, you will notice that he does not need the whole object to work. You only need the id of the element for the method areItemsTheSame()and the hash from the data of the element for the methodareContentTheSame(). This feature was used for our solution.

    From each data block of each cell, id and hash are extracted with each change. Then the resulting list of these “dried” objects is compared using DiffUtil with the same list collected from the previous adapter data. Outside it looks something like this:

    fun render(items: List) {
            adapter.setData(items)
    }

    When data is transferred to the adapter, it independently calls the notify methods for parts of the list that have changed. In addition, animations are saved whenever the list changes, whether adding a new item, deleting an item, or moving an item. There is nothing more to add here, with the example everything is clearly visible. And we will return to the disadvantages of the approach at the end of the article.

    Manage item position


    The most difficult to implement and especially the support of the adapter with a variety of cells - the methods getItemCount(), getItemViewType(), onBindViewHolder(), onCreateViewHolder(). They are interconnected and have a very specific logic. Here's what one of them might look like for a list with an optional footer and header:

        @Override
        public int getItemCount() {
            int numHeaders = userInfo == null ? 0 : 1;
            return data.size() == 0 ? numHeaders : data.size() + numHeaders + 1;
        }

    Now imagine that you need to quickly add three more types of cells, and the presence of the third depends on the presence of the first two. You will have to modify and, most unpleasantly, debug all 4 methods.

    To greatly simplify the adapter, you can use the list of objects that extend one base class or interface, and move the logic of the arrangement of elements to a higher level. But this will require wrapper objects for the data of each cell type. This is a cumbersome solution when you need, for example, to add one header.

    We took this approach for our solution, but used universal wrapper objects. This is what this class looks like:

    class Item(
            val itemController: BaseItemController,
            val data: T
    )

    The ItemController here is responsible, roughly speaking, for all interaction with the cell. We will return to him.

    As a result, everything turned out to be a little more complicated, but the essence remained the same.

    The responsibility for creating the Item list is transferred to Activity or Fragment. Now there is no need to extend RecyclerView.Adapter, because To implement this solution, a universal EasyAdapter was created. For clarity, I’ll immediately give an example of the Activity method that updates the adapter:

        fun render(screenModel: MainScreenModel) {
            val itemList = ItemList.create()
                    .addIf(screenModel.hasHeader(), headerController)
                    .addAll(screenModel.carousels, carouselController)
                    .addIf(!screenModel.isEmpty(), deliveryController)
                    .addIf(screenModel.hasCommercial, commercialController)
                    .addAll(screenModel.elements, elementController)
                    .addIf(screenModel.hasBottomCarousel(), screenModel.bottomCarousel, carouselController)
                    .addIf(screenModel.isEmpty(), emptyStateController)
            adapter.setItems(itemList)
        }

    Incidentally, this method controls the list in the gif above.

    The ItemList class is needed to make lists more convenient. A number of its features can greatly simplify this task and make the code more declarative:

    1. Chain style filling.
    2. There is no explicit creation of Item objects.
    3. Methods for adding cells without data.
    4. Methods for adding predicate cells.
    5. Methods for adding both single cells and a list of cells.

    When called adapter.setItems(), as previously mentioned, the necessary notify methods will be called.

    There is another important method in ItemList - fun addAll (data: Collection, itemControllerProvider: ItemControllerProvider): ItemList. It allows you to configure Adapter from a list of objects that extend one base class or interface. It is useful if the logic of constructing a list is nontrivial and it makes sense to transfer it to Presenter.

    Let's go back to the ItemController and immediately see an example implementation:

    class ElementController(
            val onClickListener: (element: Element) -> Unit
    ) : BindableItemController() {
        override fun createViewHolder(parent: ViewGroup): Holder = Holder(parent)
        override fun getItemId(data: Element): Long = data.id.hashCode().toLong()
        inner class Holder(parent: ViewGroup) : BindableViewHolder(parent, R.layout.element_item_layout) {
            private lateinit var data: Element
            private val nameTv: TextView
            private val coverView: ElementCoverView
            init {
                itemView.setOnClickListener { onClickListener.invoke(data) }
                nameTv = itemView.findViewById(R.id.name_tv)
                coverView = itemView.findViewById(R.id.cover_view)
            }
            override fun bind(data: Element) {
                this.data = data
                nameTv.text = data.name
                coverView.render(data.cover)
            }
        }
    }

    The first thing that catches your eye is the complete encapsulation of all interactions with the cell. This allows you to quickly and safely make changes to the list, and completely reuse the entire logic of interacting with the cell on other screens. Another ItemController is responsible for extracting from the id and hash data necessary for the automatic call of notify methods to work correctly.

    This structure allows us to simplify one more thing:

    1. No need to implement methods onBindViewHolder, getItemHash.
    2. There is no need to toss the Listener inside the Holder.
    3. An ItemController for a cell without data will be even simpler.
    4. No need to come up with names for Holder and Listener (if you still use java).
    5. You can use the ItemController template for quick implementation.

    This method allows to simplify the implementation of lists many times and unify the work with all adapters in the project. You can implement complex screens that previously had to be done using ScrollView. This allows you to buy time at the start and reduce the amount of code in the Activity. In addition, it’s easy enough to convert an existing adapter to this style. We usually do this when you need to slightly modify an existing large adapter in an old project.

    True, there are several drawbacks. For example, getChangePayloadDiffUtil.Callback is ignored, and for each cell, when changing the list, Item and ItemInfo objects are created (which, in principle, can be solved if necessary). If you are going to use gigantic lists, measure performance (DiffUtil performance can be read here) But most often you will not have problems with this approach.

    I hope my note will come in handy for some of you and allow you to spend less time and nerves working with RecyclerView.

    An example implementation with usage examples is here . There is also an ItemController template for AndroidStudio and a basic pagination adapter based on the described approaches.

    Many thanks to the developers of Surf, especially Fedor Atyakshin (@rereverse), for their help in the development.

    Also popular now: