RecyclerView at max speed: parsing libraries


    Ilya Nekrasov, Mahtalitet , KODE android developer
    For two and a half years in android development, I managed to work on completely different projects: from a social network for motorists and a Latvian bank to a federal bonus system and the third airline carrier. Anyway, in each of these projects I came across tasks that required finding non-classical solutions when implementing lists using the RecyclerView class.
    This article - the fruit of preparing for the performance at DevFest Kaliningrad'18, as well as communication with colleagues - will be especially useful for novice developers and those who have used only one of the existing libraries.


    To begin with, let's dig a little into the essence of the issue and the source of pain, namely, the expansion of the functional in developing the application and the complication of the used lists.


    Chapter One, in which the customer dreams of the application, and we - on clear requirements


    Let's imagine a situation when a customer who wants a mobile application of a shop selling rubber ducks addresses the studio.


    The project is developing rapidly, new ideas arise regularly and are not decorated in a long-term roadmap.


    First, the customer asks us to show a list of existing products and with a click to place an order for delivery. You don’t need to go far for a decision: we use the classic set from RecyclerView , a simple self-written adapter for it and an Activity .


    For the adapter, we use homogeneous data, one ViewHolder and simple logic of binding.


    Adapter with ducks
    classDucksClassicAdapter(
      privatevaldata: List<Duck>,
      privateval onDuckClickAction: (Duck) -> Unit
    ) : RecyclerView.Adapter<DucksClassicAdapter.ViewHolder>() {
      overridefunonCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val rubberDuckView = LayoutInflater.from(parent.context).inflate(R.layout.item_rubber_duck, parent, false)
        return ViewHolder(rubberDuckView)
      }
      overridefunonBindViewHolder(holder: ViewHolder, position: Int) {
        val duck = data[position]
        holder.divider.isVisible = position != 0
        holder.rubberDuckImage.apply {
          Picasso.get()
            .load(duck.icon)
            .config(Bitmap.Config.ARGB_4444)
            .fit()
            .centerCrop()
            .noFade()
            .placeholder(R.drawable.duck_stub)
            .into(this)
        }
        holder.clicksHolder.setOnClickListener { onDuckClickAction.invoke(duck) }
      }
      overridefungetItemCount() = data.count()
      classViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage)
        val clicksHolder: View = view.findViewById(R.id.clicksHolder)
        val divider: View = view.findViewById(R.id.divider)
      }
    }



    Over time, the customer has an idea to add another category of goods to the rubber ducks, which means that they will have to add a new data model and a new layout. But the most important thing is that the adapter will have another ViewType , with which you can determine which ViewHolder to use for a specific list item.


    After that, headings are added to categories, in which each category can be collapsed and expanded to simplify the orientation of users in the store. This is plus another ViewType and ViewHolder for headers. In addition, it is necessary to complicate the adapter, since it is necessary to keep a list of open groups and use it to check the need to hide and display an item by clicking on the title.


    Adapter with everything in a row
    classDucksClassicAdapter(
      privateval onDuckClickAction: (Pair<Duck, Int>) -> Unit,
      privateval onSlipperClickAction: (Duck) -> Unit,
      privateval onAdvertClickAction: (Advert) -> Unit
    ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
      vardata: List<Duck> = emptyList()
        set(value) {
          field = value
          internalData = data.groupBy { it.javaClass.kotlin }
            .flatMap { groupedDucks ->
              val titleRes = when (groupedDucks.key) {
                DuckSlipper::class -> R.string.slippers
                RubberDuck::class -> R.string.rubber_duckselse -> R.string.mixed_ducks
              }
              groupedDucks.value.let { listOf(FakeDuck(titleRes, it)).plus(it) }
            }
            .toMutableList()
          duckCountsAdapters = internalData.map { duck ->
            val rubberDuck = (duck as? RubberDuck)
            DucksCountAdapter(
              data = (1..(rubberDuck?.count ?: 1)).map { count -> duck to count },
              onCountClickAction = { onDuckClickAction.invoke(it) }
            )
          }
        }
      privateval advert = DuckMockData.adverts.orEmpty().shuffled().first()
      privatevar internalData: MutableList<Duck> = mutableListOf()
      privatevar duckCountsAdapters: List<DucksCountAdapter> = emptyList()
      privatevar collapsedHeaders: MutableSet<Duck> = hashSetOf()
      overridefunonCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        returnwhen (viewType) {
          VIEW_TYPE_RUBBER_DUCK -> {
            val view = parent.context.inflate(R.layout.item_rubber_duck, parent)
            DuckViewHolder(view)
          }
          VIEW_TYPE_SLIPPER_DUCK -> {
            val view = parent.context.inflate(R.layout.item_duck_slipper, parent)
            SlipperViewHolder(view)
          }
          VIEW_TYPE_HEADER -> {
            val view = parent.context.inflate(R.layout.item_header, parent)
            HeaderViewHolder(view)
          }
          VIEW_TYPE_ADVERT -> {
            val view = parent.context.inflate(R.layout.item_advert, parent)
            AdvertViewHolder(view)
          }
          else -> throw UnsupportedOperationException("view type $viewType without ViewHolder")
        }
      }
      overridefunonBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when (holder) {
          is HeaderViewHolder -> bindHeaderViewHolder(holder, position)
          is DuckViewHolder -> bindDuckViewHolder(holder, position)
          is SlipperViewHolder -> bindSlipperViewHolder(holder, position)
          is AdvertViewHolder -> bindAdvertViewHolder(holder)
        }
      }
      privatefunbindAdvertViewHolder(holder: AdvertViewHolder) {
        holder.advertImage.showIcon(advert.icon)
        holder.advertTagline.text = advert.tagline
        holder.itemView.setOnClickListener { onAdvertClickAction.invoke(advert) }
      }
      privatefunbindHeaderViewHolder(holder: HeaderViewHolder, position: Int) {
        val item = getItem(position) as FakeDuck
        holder.clicksHolder.setOnClickListener { changeCollapseState(item, position) }
        val arrowRes = if (collapsedHeaders.contains(item))
          R.drawable.ic_keyboard_arrow_up_black_24dp
        else
          R.drawable.ic_keyboard_arrow_down_black_24dp
        holder.arrow.setImageResource(arrowRes)
        holder.title.setText(item.titleRes)
      }
      privatefunchangeCollapseState(item: FakeDuck, position: Int) {
        val isCollapsed = collapsedHeaders.contains(item)
        if (isCollapsed) {
          collapsedHeaders.remove(item)
        } else {
          collapsedHeaders.add(item)
        }
        // 1 to add items after headerval startPosition = position + 1if (isCollapsed) {
          internalData.addAll(startPosition - ADVERTS_COUNT, item.items)
          notifyItemRangeInserted(startPosition, item.items.count())
        } else {
          internalData.removeAll(item.items)
          notifyItemRangeRemoved(startPosition, item.items.count())
        }
        notifyItemChanged(position)
      }
      @SuppressLint("SetTextI18n")privatefunbindSlipperViewHolder(holder: SlipperViewHolder, position: Int) {
        val slipper = getItem(position) as DuckSlipper
        holder.duckSlipperImage.showIcon(slipper.icon)
        holder.duckSlipperSize.text = "Размер: ${slipper.size}"
        holder.clicksHolder.setOnClickListener { onSlipperClickAction.invoke(slipper) }
      }
      privatefunbindDuckViewHolder(holder: DuckViewHolder, position: Int) {
        val duck = getItem(position) as RubberDuck
        holder.rubberDuckImage.showIcon(duck.icon)
        holder.rubberDuckCounts.adapter = duckCountsAdapters[position - ADVERTS_COUNT]
        val context = holder.itemView.context
        holder.rubberDuckCounts.layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
      }
      overridefungetItemViewType(position: Int): Int {
        if (position == 0) return VIEW_TYPE_ADVERT
        returnwhen (getItem(position)) {
          is FakeDuck -> VIEW_TYPE_HEADER
          is RubberDuck -> VIEW_TYPE_RUBBER_DUCK
          is DuckSlipper -> VIEW_TYPE_SLIPPER_DUCK
          else -> throw UnsupportedOperationException("unknown type for $position position")
        }
      }
      privatefungetItem(position: Int) = internalData[position - ADVERTS_COUNT]
      overridefungetItemCount() = internalData.count() + ADVERTS_COUNT
      classDuckViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val rubberDuckImage: ImageView = view.findViewById(R.id.rubberDuckImage)
        val rubberDuckCounts: RecyclerView = view.findViewById(R.id.rubberDuckCounts)
      }
      classSlipperViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val duckSlipperImage: ImageView = view.findViewById(R.id.duckSlipperImage)
        val duckSlipperSize: TextView = view.findViewById(R.id.duckSlipperSize)
        val clicksHolder: View = view.findViewById(R.id.clicksHolder)
      }
      classHeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val title: TextView = view.findViewById(R.id.headerTitle)
        val arrow: ImageView = view.findViewById(R.id.headerArrow)
        val clicksHolder: View = view.findViewById(R.id.clicksHolder)
      }
      classAdvertViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val advertTagline: TextView = view.findViewById(R.id.advertTagline)
        val advertImage: ImageView = view.findViewById(R.id.advertImage)
      }
    }
    privateclassFakeDuck(
      val titleRes: Int,
      val items: List<Duck>
    ) : Duck
    privatefun ImageView.showIcon(icon: String, placeHolderRes: Int = R.drawable.duck_stub) {
      Picasso.get()
        .load(icon)
        .config(Bitmap.Config.ARGB_4444)
        .fit()
        .centerCrop()
        .noFade()
        .placeholder(placeHolderRes)
        .into(this)
    }
    privateclassDucksCountAdapter(
      privatevaldata: List<Pair<Duck, Int>>,
      privateval onCountClickAction: (Pair<Duck, Int>) -> Unit
    ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
      overridefunonCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val view = parent.context.inflate(R.layout.item_duck_count, parent)
        return CountViewHolder(view)
      }
      overridefungetItemCount() = data.count()
      overridefunonBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as CountViewHolder).count.apply {
          val item = data[position]
          text = item.second.toString()
          setOnClickListener { onCountClickAction.invoke(item) }
        }
      }
      classCountViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val count: TextView = view.findViewById(R.id.count)
      }
    }

    I think you catch the essence - such a jumble reminds a little of a healthy development. And ahead there are more and more new requirements from the customer: to fix the advertising banner at the top of the list, to realize the possibility of choosing the number of ordered ducks. Only these tasks will eventually turn into successive adapters, which will again have to be written from scratch.


    The process of developing a classic adapter in githab history

    Results


    In fact, the picture is not at all encouraging: individual adapters have to be sharpened for specific cases. We all understand that there are dozens or even hundreds of such screen lists in the real application. And they do not contain information about ducks, but more complex data. Yes, and their design is much more complicated.


    What is wrong with our adapters?


    • obviously, they are difficult to reuse;
    • inside there is a business logic and with time it becomes more and more;
    • difficult to maintain and expand;
    • high risk of errors when updating data;
    • non-obvious design.

    Chapter Two, in which everything could be different


    Imagine the development of the application for years ahead is unrealistic, and meaningless. After a couple of such dances with a tambourine as in the last chapter and writing dozens of adapters, anyone will have the question “Maybe there are other solutions?”.



    Having protested Github, we find out that the first AdapterDelegates library appeared in 2015 , and a year later, Groupie and Epoxy added to the arsenal of developers - they all help make life easier, but each has its own specifics and pitfalls.


    There are several other similar libraries (for example, FastAdapter), but neither I nor my colleagues worked with them, so we will not discuss them in the article.

    Before comparing libraries, we briefly analyze the case described above with an online store, provided the AdapterDelegates is used - from the libraries being parsed it is the easiest from the point of view of internal implementation and use (however, it is not completely advanced, so much has to be added by hand).


    The library does not completely save us from the adapter, but it will be formed from blocks (bricks), which we can safely add to the list or remove from it and change their places.


    Block Adapter
    classDucksDelegatesAdapter : ListDelegationAdapter<List<DisplayableItem>>() {
      init {
        delegatesManager.addDelegate(RubberDuckDelegate())
      }
      funsetData(items: List<DisplayableItem>) {
        this.items = items
        notifyDataSetChanged()
      }
    }
    privateclassRubberDuckDelegate : AbsListItemAdapterDelegate<RubberDuckItem, DisplayableItem, RubberDuckDelegate.ViewHolder>() {
      overridefunisForViewType(item: DisplayableItem, items: List<DisplayableItem>, position: Int): Boolean {
        return item is RubberDuckItem
      }
      overridefunonCreateViewHolder(parent: ViewGroup): ViewHolder {
        val item = parent.context.inflate(R.layout.item_rubber_duck, parent, false)
        return ViewHolder(item)
      }
      overridefunonBindViewHolder(item: RubberDuckItem, viewHolder: ViewHolder, payloads: List<Any>) {
        viewHolder.apply {
          rubberDuckImage.showIcon(item.icon)
        }
      }
      classViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val rubberDuckImage: ImageView = itemView.findViewById(R.id.rubberDuckImage)
      }
    }

    Using Adapter Activity
    classDucksDelegatesActivity : AppCompatActivity() {
      privatelateinitvar ducksList: RecyclerView
      overridefunonCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ducks_delegates)
        ducksList = findViewById(R.id.duckList)
        ducksList.apply {
          layoutManager = LinearLayoutManager(this@DucksDelegatesActivity)
          adapter = createAdapter().apply { showData() }
        }
      }
      funcreateAdapter(): DucksDelegatesAdapter {
        return DucksDelegatesAdapter()
      }
      privatefun DucksDelegatesAdapter.showData() {
        setData(getRubberDucks())
      }
      privatefungetRubberDucks(): List<DisplayableItem> {
        return DuckMockData.ducks.orEmpty().map {
          RubberDuckItem(it.icon)
        }
      }
    }

    Already from the first task we see the difference: we have an adapter class that is inherited from the library. And in addition - the very brick that is called a delegate and from which we also inherit and implement some of the logic we need. Next we add a delegate to the manager - this is also a library class. And the last thing to do is create an adapter and fill it with data.


    To implement the second category of store and headers, we will write a couple of delegates, and the animation comes from the DiffUtil class .


    Here I’ll mark a brief but categorical conclusion: the use of even this library solves all the listed problems that arose when the application became more complex in a case with an online store, but without any drawbacks anywhere, and more about them.


    Adapter development process with AdapterDelegates in github history

    Chapter Three, in which the developer removes rose-colored glasses by comparing libraries


    Immerse more in the functionality and operation of each of the libraries. I somehow applied all three libraries on our projects, depending on the tasks and complexity of the application.


    AdapterDelegates


    We use this library in the application of one of the largest Russian airlines. We needed to replace the simple pay list with a list with groups and a large number of different parameters.


    Simplified scheme of the library looks like this:


    The main class is DelegateAdapter , the various “bricks” are the “delegates” who are responsible for displaying a particular data type and, of course, the list itself.


    Pros:


    • ease of immersion;
    • easy to reuse adapters;
    • few methods and classes;
    • no reflection, code generation and databing.

    Minuses:


    • you need to implement the logic yourself, for example update items via DiffUtil (since version 3.1.0, you can use the AsyncListDifferDelegationAdapter adapter);
    • redundant code.

    In general, this library solves all the main difficulties in expanding the functionality of the application and is suitable for those who have not used the library before. But I don’t recommend to dwell only on it.

    Groupie


    Groupie , created a few years ago by Lisa Wray , we often use, including completely using it to write an application for one Latvian bank.


    In order to use this library, first of all you need to deal with dependencies . In addition to the main one, you can use several options to choose from:



    We stop at one and write the necessary dependencies.


    Using the example of an online shop with ducks, we need to create an Item inherited from the library class, specify the layout and implement the binding through the Kotlin syntenty. If you compare it with the amount of code that you had to write with the AdapterDelegates , it’s just heaven and earth.


    All that remains is to set the RecyclerView GroupieAdapter as an adapter and put matched items into it.



    It is seen that the scheme of work is more and more difficult. Here, in addition to simple items, you can use whole sections - groups of items and other classes.


    Pros:


    • friendly interface, although api makes you think;
    • availability of boxed solutions;
    • breakdown into groups of elements;
    • the choice between the usual option, Kotlin Extensions and DataBinding;
    • embedding ItemDecoration and animation.

    Minuses:


    • incomplete wiki;
    • poor support by the maintainer and the community;
    • small bugs that had to be circumvented in the first version;
    • diffing in main thread (for now);
    • no support for AndroidX (at the moment, but you need to keep track of the repository).

    It is important that Groupie, with all its drawbacks, is able to easily replace AdapterDelegates , especially if you plan to make roll-up lists of the first level, and do not want to write a lot of boilerplate.


    Implementing a duck list using Groupie

    Epoxy


    The latest library, which we began to use relatively recently, is Epoxy , developed by the guys from Airbnb . The library is complicated, but it allows you to solve a whole task load. Airbnb programmers themselves use it to render screens directly from the server. We Epoxy handy on one of the most recent projects - an application for the bank in Yekaterinburg.


    To develop screens, we had to work with different types of data, a huge number of lists. And one of the screens was really endless. And Epoxy helped us all with this .


    The principle of the library as a whole is similar to the two previous ones, except that instead of the adapter, the EpoxyController is used to build the list, which allows you to define the adapter structure declaratively.



    To achieve this, the library is built on code generation. How it works - with all the nuances well described in the wiki and reflected in the samples .


    Pros:


    • list models generated from ordinary View, with the possibility of reuse in simple screens;
    • declarative description of the screens;
    • DataBinding at max speeds - generates models directly from layout files;
    • just displaying from the blocks not only lists, but also complex screens;
    • General ViewPool on Activity;
    • asynchronous diffing out of the box (AsyncEpoxyController);
    • No need to mess with horizontal lists.

    Minuses:


    • a heap of classes, processors, annotations;
    • difficult dive from scratch;
    • uses ButterKnife plugin to generate R2 files in modules;
    • it is very difficult to understand how to work with Callbacks correctly (we ourselves have not understood yet);
    • There are problems that need to be circumvented: for example, a crash with the same id.

    Implementing a duck list with Epoxy

    Results


    The main thing that I wanted to convey: you should not put up with the complexity that appears when you need to make complex lists and constantly have to redo them. And this happens very often. And in principle, when they are implemented, if the project only starts, or you are engaged in its refactoring.


    The reality is that you shouldn’t complicate the logic once again, thinking that it’s enough to have some kind of own abstractions. They are enough for a short time. But working with them is not only not enjoyable, it also remains a temptation to transfer part of the logic to the UI-part, which should not be there. There are tools that will help avoid most problems, and they need to be used.


    I understand that for many experienced (and not only) developers, this is either obvious, or they may not agree with me. But I consider it important to re-emphasize this.

    So, what to choose?


    It is rather difficult to advise on one library, because the choice depends on many factors: from personal preferences to ideology on the project.


    I would do the following:


    1. If you are just starting your way into development, try starting on a small project with AdapterDelegates - this is the easiest library - you won't need much knowledge. Understand how to work with it and why it is more convenient than writing adapters yourself.
    2. Groupie is suitable for those who have already played enough with the AdapterDelegates and are tired of writing a bunch of boilerplate, or for everyone else who wants to start right from the middle. And do not forget about the presence of folding groups out of the box - this is also a good argument in its favor.
    3. Well, and Epoxy - for those who are faced with a truly complex project, with a huge amount of data, so that the complexity of the library will be less of a problem. At first it will be hard, but then the implementation of the lists will seem like a damn. An important argument in favor of Epoxy could be the presence of a DataBinding and MVVM on the project - it is literally created for this, given the possibility of generating models from the corresponding layouts.

    If you have any questions, you can look at the link to once again see the code of our application with the ducks.


    Also popular now: