Smooth RecyclerView Scrolling in Android
Tips for using complex views without skipping frames
The problem
RecyclerViews are one of the most demanding widgets on Android, and getting smooth scroll performance with complex views can be a struggle. Most devices run at a refresh rate of 60 frames per second. This means there is a new frame every ~16ms. If your app takes longer than 16ms to figure out what the next frame should display, your app skips a frame, and somewhere a puppy dies.
Measuring scroll performance
One of the coolest ways to see if your app is skipping frames is to use the Profile GPU Rendering tool. This gives you a nice visualization in real time that will show you when frames are getting skipped. To enable it:
- On your device, go to Settings and tap Developer Options.
- In the Monitoring section, select Profile GPU Rendering.
- In the Profile GPU Rendering dialog, choose On screen as bars to overlay the graphs on the screen of your device.
- Open the app that you want to profile.
Pre-caching views in the Adapter
One bottleneck that can occur when scrolling through your RecyclerView is ViewHolder creation, via onCreateViewHolder()
. As you scroll, your adapter will create new ViewHolders on demand, until it has enough of them to recycle.
View inflation is slow. If your layout XML is complex it could take longer than 16ms to inflate, causing a frame skip. What if we could inflate the views ahead of time, before we display the list to the user? We can do this withAsyncLayoutInflater
to inflate our views on a background thread and get a callback when they are ready.
Note that in this example we are inflating the views right when the adapter instance is created. This will only work if there is enough time to inflate the views before we call submitList
on the adapter. Otherwise if we call submitList
right after creating our adapter, there won’t be time to cache views and this example won’t help. For that case, you will need to set a callback on your adapter to notify that view caching is complete, then call submitList
.
Spreading out work across multiple frames
Another area that might lead to slow performance is when onBindViewHolder
is called. Since this is usually called for each row right before it comes into view, it needs to be fast. But sometimes a complicated view might have too much stuff going on. If calling onBindViewHolder
takes longer than 16ms to complete, we will see jank when scrolling.
We can improve this by creating a UI job scheduler. In your ViewHolder, break up your bind view code into a few functions — maybe one for setting text, one for setting images. We want to make sure each step completes in a short amount of time, like less than 8ms.
The Job scheduler will run each job you submit in sequence and keep track of how long it takes. Once it reaches close to the maximum allotted time per frame, it will wait until the next frame to continue processing jobs. That way the UI has room to “breathe” and render the next frame. In practice, I find best results using a maximum time of 4ms per frame, far less than the available 16ms per frame. But you can try adjusting it to see what works for you. It looks like this:
The usage is simple:
class ListItemViewHolder(view: View) : ViewHolder(view) {
fun populateFrom(listItem: ListItem) {
UIJobScheduler.submitJob { setupText() }
UIJobScheduler.submitJob { setupText2() }
UIJobScheduler.submitJob { setupImage2() }
}
}
Conclusion
onCreateViewHolder()
and onBindViewHolder()
are the two areas of our RecyclerView
adapter that can be slow and cause jank as we scroll. We can improve onCreateViewHolder
performance by caching our views ahead of time, and we can improve onBindViewHolder
performance by spreading the work out across multiple frames, via our UIJobScheduler
singleton.
Another option
Facebook created their own library, called Litho to improve their scrolling performance. It is a different paradigm and might require a significant refactoring of your existing code, but it does appear to work quite well.