sunakulto
sunakulto

Reputation: 71

Can't get initialPrefetchItemCount to work, or how can I optimize scrolling in RecyclerView?

I have a RecyclerView with a complex layout. It's a vertical list where each item contains a nested RecyclerView with a GridLayoutManager. The inner list can have up to 40 items. Additionally, the outer list items may contain other nested lists, but for brevity, let's focus on the primary structure:

So far, I've applied several optimizations to improve scrolling performance:

In the end of the question, I’ve provided a code sample replicating my setup, so you can copy paste it and run, if you wish.

The issue:

When the list first appears on screen, the outer RecyclerView inflates its first item. Due to the size of the inner grid items, only one outer item is fully visible. As expected, it inflates 40 inner items.

However, when I scroll down and a second outer item appears, it inflates another 40 inner items, causing a noticeable lag. After this initial lag, scrolling becomes perfectly smooth because the RecycledViewPool is filled, reducing excessive inflations.

My attempted solution:

I tried using initialPrefetchItemCount, as suggested in the Android documentation: https://developer.android.com/topic/performance/vitals/render#recyclerview_nested_recyclerviews

So I added this to my inner RecyclerView:

innerRecyclerView.layoutManager = GridLayoutManager(itemView.context, 4).apply {
    recycleChildrenOnDetach = true
    isItemPrefetchEnabled = true
    initialPrefetchItemCount = 40
}

However, this made scrolling even worse. From the logs, I noticed additional unwanted inflations happening.

My questions:

Any insights or recommendations would be greatly appreciated!

The code

Inner adapter

class InnerAdapter : ListAdapter<InnerItem, RecyclerView.ViewHolder>(InnerDiffCallback()) {

    init {
        setHasStableIds(true)
    }

    companion object {
        const val TYPE_ONE = 1
        const val TYPE_TWO = 2
        const val TYPE_THREE = 3
        const val TYPE_FOUR = 4
    }

    override fun getItemId(position: Int) = getItem(position).id

    override fun getItemViewType(position: Int) = when (getItem(position)) {
        is InnerItemTypeOne -> TYPE_ONE
        is InnerItemTypeTwo -> TYPE_TWO
        is InnerItemTypeThree -> TYPE_THREE
        is InnerItemTypeFour -> TYPE_FOUR
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
        TYPE_ONE -> {
            Log.d("inner-inflate-debug", "Inflated: InnerViewHolderTypeOne")
            InnerViewHolderTypeOne(
                LayoutInflater.from(parent.context).inflate(
                    R.layout.list_item_inner_one,
                    parent,
                    false
                )
            )
        }

        TYPE_TWO -> {
            Log.d("inner-inflate-debug", "Inflated: InnerViewHolderTypeTwo")
            InnerViewHolderTypeTwo(
                LayoutInflater.from(parent.context)
                    .inflate(R.layout.list_item_inner_two, parent, false)
            )
        }

        TYPE_THREE -> {
            Log.d("inner-inflate-debug", "Inflated: InnerViewHolderTypeThree")
            InnerViewHolderTypeThree(
                LayoutInflater.from(parent.context)
                    .inflate(R.layout.list_item_inner_three, parent, false)
            )
        }

        TYPE_FOUR -> {
            Log.d("inner-inflate-debug", "Inflated: InnerViewHolderTypeFour")
            InnerViewHolderTypeFour(
                LayoutInflater.from(parent.context)
                    .inflate(R.layout.list_item_inner_four, parent, false)
            )
        }

        else -> throw IllegalArgumentException("Invalid view type")
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = getItem(position)
        when (holder) {
            is InnerViewHolderTypeOne -> holder.bind(item as InnerItemTypeOne)
            is InnerViewHolderTypeTwo -> holder.bind(item as InnerItemTypeTwo)
            is InnerViewHolderTypeThree -> holder.bind(item as InnerItemTypeThree)
            is InnerViewHolderTypeFour -> holder.bind(item as InnerItemTypeFour)
        }
    }

}

class InnerViewHolderTypeOne(view: View) : RecyclerView.ViewHolder(view) {
    private val textView: MaterialTextView = view.findViewById(R.id.textView)
    fun bind(item: InnerItemTypeOne) {
        textView.text = "${item.id}"
    }
}

class InnerViewHolderTypeTwo(view: View) : RecyclerView.ViewHolder(view) {
    private val textView: MaterialTextView = view.findViewById(R.id.textView)
    fun bind(item: InnerItemTypeTwo) {
        textView.text = "${item.id}"
    }
}

class InnerViewHolderTypeThree(view: View) : RecyclerView.ViewHolder(view) {
    private val textView: MaterialTextView = view.findViewById(R.id.textView)
    fun bind(item: InnerItemTypeThree) {
        textView.text = "${item.id}"
    }
}

class InnerViewHolderTypeFour(view: View) : RecyclerView.ViewHolder(view) {
    private val textView: MaterialTextView = view.findViewById(R.id.textView)
    fun bind(item: InnerItemTypeFour) {
        textView.text = "${item.id}"
    }
}

class InnerDiffCallback : DiffUtil.ItemCallback<InnerItem>() {
    override fun areItemsTheSame(oldItem: InnerItem, newItem: InnerItem) =
        oldItem.id == newItem.id

    override fun areContentsTheSame(oldItem: InnerItem, newItem: InnerItem) =
        oldItem == newItem
}

Outer adapter

class OuterAdapter(
    private val innerItemViewPool: RecyclerView.RecycledViewPool
) : ListAdapter<OuterItem, OuterViewHolder>(OuterDiffCallback()) {

    init {
        setHasStableIds(true)
    }

    override fun getItemId(position: Int) = getItem(position).id

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

    override fun onBindViewHolder(holder: OuterViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

}

Models:

class OuterViewHolder(
    view: View,
    innerItemViewPool: RecyclerView.RecycledViewPool
) : RecyclerView.ViewHolder(view) {

    private val textView: TextView = view.findViewById(R.id.outerTextView)
    private val innerRecyclerView: RecyclerView = view.findViewById(R.id.innerRecyclerView)
    private val adapter = InnerAdapter()

    init {
        innerRecyclerView.layoutManager = GridLayoutManager(itemView.context, 4).apply {
            recycleChildrenOnDetach = true
            isItemPrefetchEnabled = true
            initialPrefetchItemCount = 40
        }
        innerRecyclerView.adapter = adapter
        innerRecyclerView.setRecycledViewPool(innerItemViewPool)
    }

    fun bind(outerItem: OuterItem) {
        textView.text = outerItem.text
        adapter.submitList(outerItem.innerItems)
    }
}

class OuterDiffCallback : DiffUtil.ItemCallback<OuterItem>() {
    override fun areItemsTheSame(oldItem: OuterItem, newItem: OuterItem) = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: OuterItem, newItem: OuterItem) = oldItem == newItem
}

Models

sealed interface InnerItem {
    val id: Long
}

data class InnerItemTypeFour(override val id: Long) : InnerItem

data class InnerItemTypeOne(override val id: Long) : InnerItem

data class InnerItemTypeThree(override val id: Long) : InnerItem

data class InnerItemTypeTwo(override val id: Long) : InnerItem

data class OuterItem(val id: Long, val text: String, val innerItems: List<InnerItem>)

Activity:

class MainActivity : AppCompatActivity() {

    private val innerItemViewPool = RecyclerView.RecycledViewPool().apply {
        setMaxRecycledViews(InnerAdapter.TYPE_ONE, 20)
        setMaxRecycledViews(InnerAdapter.TYPE_TWO, 20)
        setMaxRecycledViews(InnerAdapter.TYPE_THREE, 20)
        setMaxRecycledViews(InnerAdapter.TYPE_FOUR, 20)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_main)
        setInsets()
        val items = makeItems()
        val adapter = OuterAdapter(innerItemViewPool)
        val rv = findViewById<RecyclerView>(R.id.recycler)
        rv.layoutManager = LinearLayoutManager(this)
        rv.adapter = adapter
        rv.setHasFixedSize(true)
        adapter.submitList(items)
    }

    private fun makeItems(): MutableList<OuterItem> {
        val items = mutableListOf<OuterItem>()
        for (i in 0..99) {
            val innerItems = mutableListOf<InnerItem>()
            for (j in 0..39) {
                val id = (i + 1).toString() + (j + 1).toString()
                val innerItem = when (j % 4) {
                    0 -> InnerItemTypeOne(id.toLong())
                    1 -> InnerItemTypeTwo(id.toLong())
                    2 -> InnerItemTypeThree(id.toLong())
                    3 -> InnerItemTypeFour(id.toLong())
                    else -> throw RuntimeException()

                }
                innerItems.add(innerItem)
            }
            val outerItem = OuterItem(i.toLong(), "Hey, I'm item #$i", innerItems)
            items.add(outerItem)
        }
        return items
    }

    private fun setInsets() {
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
    }
}

XML layouts:

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

list_item_inner_four.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/textView"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:gravity="center"
    android:textColor="@color/black"
    android:background="@android:color/holo_purple"
    tools:text="ID: 123" />

list_item_inner_one.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/textView"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:textColor="@color/black"
    android:background="@android:color/holo_green_light"
    android:gravity="center"
    tools:text="ID: 123" />

list_item_inner_three.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/textView"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:textColor="@color/black"
    android:background="@android:color/holo_red_light"
    android:gravity="center"
    tools:text="ID: 123" />

list_item_inner_two.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/textView"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@android:color/holo_blue_bright"
    android:gravity="center"
    android:textColor="@color/black"
    tools:text="ID: 123" />

list_item_outer.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginHorizontal="16dp"
    android:layout_marginVertical="8dp"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        android:orientation="vertical">

        <TextView
            android:id="@+id/outerTextView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:textStyle="bold"
            tools:text="Hello" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/innerRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:nestedScrollingEnabled="false" />

    </LinearLayout>

</com.google.android.material.card.MaterialCardView>

Upvotes: 0

Views: 30

Answers (0)

Related Questions