Fantastic RecyclerView.ViewHolder and where to create them

Sergey Lapin
Life at Vivid

--

Let’s assume that you’ve already optimized your RecyclerView back and forth:

  • setHasFixedSize(true)
  • DiffUtil
  • views are more flat than Earth
  • fastest ViewHolder binding in the Wild West

But you aren’t satisfied with that and eager to find new ways of optimization. Congrats, you’ve landed in the right article!

Background

Once upon a time, while painting and moving buttons, I noticed that RecyclerView on one of our screens was skipping frames a bit during active scrolling due to big variety of viewTypes used (and therefore frequent ViewHolder creation). That reminded me about old article by Chet Haase about introducing prefetching mechanism to RecyclerView in order to prefetch items that are about to appear on screen during idle gaps on main thread. But it seemed to me that this wasn't enough, so I decided to try to create items during idle gaps on... background thread.

That’s how the idea came about to tune work of RecyclerView.RecycledViewPool so that it would fill itself with views created off the main thread and offer them to RecyclerView on demand (i.e. prefetch).

Aside of performance benefits for scrolling through lists with big variety of elements, one could also get elements from that mechanism when, for example, first page of content is being loaded and views for that content can be created in advance, therefore speeding up their showing to the user.

I decided to break up that idea in two pieces: the first piece will do all the heavy-lifting on creating the views in background (Supplier), second will receive them and offer to RecyclerView on demand (Consumer)

RecyclerView.RecycledViewPool

To begin with, it’s worth understanding what RecyclerView.RecycledViewPool is, in order to build our Consumer on its basis.

RecycledViewPool — is a storage for... recycled views, from which we can retrieve them for the needed viewType, thereby not creating them once again. Thus, it is the RecycledViewPool that underlies famous RecyclerView's performance. In addition to just storing recycled views RecycledViewPool also stores information about approximate time required for view creation and binding — so that in the future, at the request of GapWorker, it is plausible enough to "predict" whether it will be possible to effectively utilize the remaining time in the frame in order to preemptively create a view of a particular viewType.

Consumer

Full Consumer code

Now, when we understand what is RecyclerView.RecycledViewPool — we can start modifying its functionality in order to conform our needs (filling in advance not only by the request of GapWorker, but from out Supplier as well)

First of all, we’d want to extend an API of our prefetcher with an ability to configure amount of views of desired type, which are to be… prefetched.

Besides saving the value of maximum number of stored views we are also informing ViewHolderSupplier about amount of views that we'd like to eventually get (by calling setPrefetchBound) and that actually triggers the process of prefetching.

Then, when recycler will try to recycle view, we are going to update view pool with maximum amount of stored views so as not to store redundant views and to actualize the knowledge about that number since it can vary over time.

After that, when our view pool will get a request from recycler for retrieving a recycled view and we’ll not have one (previously recycled or prefetched) at its disposal then we’ll notify our Supplier that recycler is going to create another view of that type on main thread.

The only thing that’s left is to connect lifecycle of our view pool and the Supplier, and determine that our view pool is going to be a Consumer for our prefetched views:

Method factorInCreateTime worth mentioning here. It's a method that saves time of view holder creation and with that knowledge GapWorker is going to make assumptions whether it's going to be able to prefetch views during gap between frames or not.

Supplier

Full Supplier code

Now, that we’ve dealt with the first part of our tool, let’s dive into implementation of the second one — the Supplier. Its main goal is to enqueue item creation for certain viewType, create them somewhere off the main thread and pass them to the Consumer. Apart from that, in order to avoid unnecessary work it should react to the view being created outside of it in a way that it will decrease the amount of enqueued creations.

Launching the queue

In order to launch the queue of item creation we check if there is enough items already enqueued. Then we check if we’ve already created enough views of that type. In case if both of these conditions meet — we can enqueue request for creating an amount of views that would be sufficient to reach target value of count:

Creating views

As for the enqueueItemCreation method — it will be abstract and its implementation will depend on the approach to multithreading chosen by your team.

But what will definitely not be abstract is the createItem method, which is supposed to be called from somewhere outside the main thread inside implemented enqueueItemCreation. In this method, first of all, we check whether something needs to be done or we already have a sufficient number of cached views? Then we create our view, remembering the time spent on its creation, ignoring errors (let them fail on main thread). After that, we will inform the new viewholder of its viewType, just so that he is aware, we will make a note that we have created another element with the necessary viewType and notify Consumer about the same:

Here viewHolderProducer is just a simple typealias:

(e.g. RecyclerView.Adapter.onCreateViewHolder can do the job)

It’s worth mentioning that traditional view creation via LayoutInflater.inflate might be not the most effective way of utilising capabilities of the described above mechanism due to synchronization on constructor's arguments...

React on view created outside of Supplier

In case if view was created outside of our Supplier (onItemCreatedOutside method) we will just update current amount of total views created for that type:

Profit!

Now you just need to determine the behavior of the start, stop and enqueueItemCreation methods, depending on the approach chosen by your team to work with multithreading, and you will get a wunderwafl for creating viewholders outside the main thread, which can even be reused between screens and supplied to different recyclers using the same adapters/views, for example...

In case you don’t want to think about how to write your own implementation of ViewHolderSupplier, then, just today, especially for you, I made a small library, which has an artifact with the 'core' functionality described in this article, as well as artifacts for all the main approaches to modern multithreading (Kotlin Coroutines, RxJava2, RxJava3, Executor).

--

--