Reduce the number of inflation of ViewHolders drastically by sharing a ViewPool across multiple RecyclerViews.
This is a translated article of https://qiita.com/chibatching/items/19ec43c62db2e38ce673. All the credits go to @chibatching
It was my second DroidKaigi attendance and I really enjoyed it because I felt I was in a festival surrounded by lots of Android developers. In this article, I’m going to explain about the Pull Requests I sent to the official DroidKaigi 2018 app on a shared RecycledViewPool.
What motivated me
I learnt that a RecyclerViewPool can be shared across multiple RecyclerViews in the talk at DroidKaigi 2018 presented by @thagikura titled as “Deep Dive into LayoutManager for RecyclerView”. Then I started thinking would it optimize a case where each page in a ViewPager consists of similar Views by sharing a common ViewPool?
By looking at the document of the RecyclerView#setRecycledViewPool, it says:
Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views. This can be useful if you have multiple RecyclerViews with adapters that use the same view types, for example if you have several data sets with the same kinds of item views displayed by a ViewPager.
Yay, looks like it’s intended to be used for exactly the case I imagined.
At first I tried to use it for our production app, but I gave up because of an issue (I’ll talk about it later). But soon noticed there was an app (the official DroidKaigi app) that was structured exactly what I wanted.
Translator’s note: The DroidKaigi 2018 app has two modes that can be toggled by clicking the top right button. - Sessions are aligned by Room - Sessions are aligned by time slot And both have the ALL tab as the most left tab like the video below.
It’s very simple to to share a ViewPool, you can just pass an instance of RecycledViewPool by calling RecyclerView#setRecycledViewPool. But that didn’t much optimize the case as I expected. Then it turned out there is a method called setRecycleChildrenOnDetach in LayoutManagers such as LinearLayoutManager or GridLayoutManager.
Set whether LayoutManager will recycle its children when it is detached from RecyclerView.
If you are using a RecyclerView.RecycledViewPool, it might be a good idea to set this flag to true so that views will be available to other RecyclerViews immediately.
Note that, setting this flag will result in a performance drop if RecyclerView is restored.
Looks like detached children are not going to be available from other RecyclerViews unless this flag is set to true.
The implementation looks like as follows including the flag.
Let’s make the three fragments, AllSessionsFragment.kt, RoomSessionsFragment.kt and ScheduleSessionsFragment.kt to share the common ViewPool and change the GroupAdapter as follows to count how many times the ViewHolders are inflated.
I compared how many times the ViewHolders are inflated before and after the change that shares the common ViewPool by following operations.
- Launch the app (“ALL” tab should be active and sessions are aligned by room)
- Switch the tab from “ROOM 1” to “ROOM 7”
- Tap the button to toggle the sessions aligned by a time slot
- Switch the tab from “Day2 14:00” to “ALL”
Whoa, the number of inflation dropped to one third!! Turned out that optimization is really effective for that type of app that uses multiple RecyclerViews where similar views are used across them. (Of course the effectiveness should differ depending on how the app uses RecyclerView)
Few things to note
You need to make sure the same ViewType needs to be used across RecyclerViews that shares the common ViewPool
This was the reason that this optimization couldn’t be applied to our production app. When a ViewHolder is searched from the RecycledViewPool, it’s looked up by the ViewType that the adapter returns by getItemViewType. Therefore, if you assign the same ViewType to different Views across the RecyclerViews, the layout is going to be broken miserably.
I gave up applying the optimization for our app because the custom adapter we used for our production app assigned a ViewType dynamically.
On the other hand, Groupie the DroidKaigi app uses, assign a ViewType from the layout ID of the View, which made the optimization applicable in that regard.
You need to be careful about the lifecycle and how you share the RecycledViewPool
RecycledViewPool doesn’t have a Context in it, but storing a View inside of it is equivalent to having a Context of the Activity. Therefore the the shared Views should be within the same Activity (I didn’t verify what happens if you share the Views that span across multiple Activities though).
In addition, it may result in memory leaks if the RecycledViewPool live longer than the Activity. You MUST NOT share a RecycledViewPool stored in a singleton class.
In the Pull Request I sent at first, the RecycledViewPool was stored within a ViewModel, which is also discouraged. (see https://developer.android.com/topic/libraries/architecture/viewmodel.html)
Caution: A ViewModel must never reference a view, Lifecycle, or any class that may hold a reference to the activity context.
The one that introduced a shared RecycledViewPool: https://github.com/DroidKaigi/conference-app-2018/pull/660
The one that changed how the RecycledViewPool is provided (Dagger injection instead of being stored in a ViewModel). https://github.com/DroidKaigi/conference-app-2018/pull/662
I hadn’t had a chance to contribute to the DroidKaigi app before this optimization since I was swamped by work and personal matters. But glad to send Pull Requests at the last minute.
(Isn’t this life-changing because there are tons of apps that have a ViewPager where each page consists of similar Views?)
As the leader of the DroidKaigi app says, it’s a common pattern and the amount of work required for this optimization is small. I highly recommend to give it a try if you have an app, which this optimization can be applicable.