Mastering RecyclerView Optimizations in Android

Bal sikandar
5 min readSep 3, 2023

--

RecyclerView is a powerful widget in Android for efficiently displaying large lists or grids of data. To ensure your app’s performance and responsiveness, it’s crucial to optimize your RecyclerView implementation. In this article, we’ll explore various techniques and concepts that will help you make the most out of RecyclerView.

RecyclerView Pool and When to Use It

The RecyclerView Pool is a mechanism that helps manage the memory and performance of the views within the RecyclerView. It’s essentially a cache that holds views that are currently not visible on the screen but might be needed again in the near future. This significantly reduces the overhead of inflating new views every time a new item enters the visible area.

When to Use RecyclerView Pool:

  • Use it when dealing with large lists or grids of data.
  • Opt for RecyclerView Pool when the items in your list have varying view types.

onCreateViewHolder and onBindViewHolder

In the RecyclerView, onCreateViewHolder and onBindViewHolder are essential methods in the adapter. They work together to create and bind views for each item in the list.

  • onCreateViewHolder is called when a new view needs to be created. It's responsible for inflating the layout and creating a ViewHolder instance.
  • onBindViewHolder is called when an existing view is reused for a new item. It binds the data to the view holder, allowing you to update the content of the view.
  • onBindViewHolder should not be used to bind click listeners and we should be mindful of what anonymous object or task we do within it. Read the code below carefully and then I’ll explain why?
class MyAdapter(private val items: List<Item>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {

inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
init {
itemView.setOnClickListener {
// Handle item click here
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val clickedItem = items[position]
// Handle click event for clickedItem
}
}
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = items[position]
// Bind data to the view holder here
}

override fun getItemCount() = items.size
}
  • In onBindViewHolder, click listeners are generated individually for each item, leading to multiple listener instances. By placing click listeners in ViewHolder, they are formed once upon ViewHolder creation, enhancing memory efficiency due to ViewHolder reusability.

Different Ways to Optimize RecyclerView

  1. Use Image Libraries: When dealing with images, it’s recommended to use image libraries that provide a bitmap pool. This prevents excessive memory usage and frequent garbage collection.
Glide.with(this)
.load(imageUrl)
.placeholder(R.drawable.placeholder_image)
.error(R.drawable.error_icon)
.into(imageView)
  1. Optimize Image Size: Obtain image dimensions and aspect ratios from the server to avoid unnecessary resizing and scaling.
  2. setHasStableIds: This method should be used setHasStableIds(true) to enable stable item IDs. This helps in efficiently updating and reordering items without unnecessary rebinds.
class MyAdapter(private val itemList: List<Item>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {

init {
setHasStableIds(true) // Enable stable IDs for this adapter
}

//....
}
  1. setHasFixedSize: If RecyclerView size itself is fixed and wouldn’t change due to its content and, using setHasFixedSize(true) can help improve performance by avoiding unnecessary layout calculations. Reference: Link
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.setHasFixedSize(true) // Enable fixed size for the RecyclerView
  1. setItemViewCacheSize: Adjust the cache size using this method to control how many offscreen views are retained. This can help in managing memory usage.
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.setItemViewCacheSize(10) // Set the view cache size to custom value depending on item sizes

Invalidating Views vs. Requesting Layout

  • Invalidate: It’s used to indicate that a view’s content has changed and needs to be redrawn.
  • requestLayout: It’s used to request a new layout pass, affecting the size and position of views.

You might be wondering why I'm suddenly talking about invalidate vs requestLayout. Let me explain.

I discussed “invalidate” vs “request layout” to illustrate how each affects a view’s lifecycle. This impacts screen redraw time, where excessive layout and draw phases can slow performance. To optimize Android pages, especially with RecyclerView, a thorough grasp of view lifecycles is essential.

Let me use an example to explain it further:

What will or should be called(invalidate or requestLayout) when TextView has wrap_content vs. 100dp fixed?

Answer:

When a TextView has its width set to wrap_content, calling requestLayout() would be more appropriate. This is because requestLayout() signals that the view hierarchy needs to be remeasured and laid out, ensuring the content fits correctly within the bounds.

When a TextView’s width is fixed (e.g., set to a specific value like 100dp), calling invalidate() is generally sufficient. It informs the system that the view's content has changed, and it needs to be redrawn within its existing layout bounds. This avoids unnecessary re-layout calculations while updating the view's appearance.

ViewHolder Pattern

The ViewHolder pattern is a design pattern that enhances recyclerview's performance by minimizing the number of calls to findViewById(). It involves creating a ViewHolder class that holds references to the views within an item's layout. This allows for efficient reuse of views as the user scrolls through the list.

For a comprehensive example of the ViewHolder pattern, check out this link.

Default Scrap and Dirty ViewTypes

In a RecyclerView, the default count of scrap and dirty views is set to 5 for each view type. This universal number might not be optimal for scenarios like headers, where only 1 or 2 scrap views suffice. Scrap views are reusable, detached views that can be easily reattached.

To enhance memory efficiency, you can tailor the scrap view behavior by customizing the pool size for each specific view type based on its prevalence. This approach ensures that memory usage is optimized by effectively managing the number of scrap views for different types of items in the RecyclerView.

val customRecycledViewPool = RecyclerView.RecycledViewPool().apply {
setMaxRecycledViews(viewType, poolSize) // Set your custom viewType and poolSize
}

Conclusion

Optimizing your RecyclerView implementation is crucial for maintaining a smooth and responsive user experience. By understanding concepts like RecyclerView Pool, and ViewHolder pattern, and optimizing view creation and binding, you can ensure that your app efficiently handles large lists of data.

Remember that each optimization technique serves a specific purpose, and applying a combination of these techniques can significantly enhance your RecyclerView’s performance.

Thanks for reading this article. Be sure to 👏 recommend this article if you found it helpful. It means a lot.

Also let’s connect on twitter, github and linkedin.

Clap, share if you like it, and follow me for my next move.

--

--

Bal sikandar

Android developer @shuttl Ex @_okcredit. Blogger | Open source contributor https://about.me/balsikandar.