Android knowledge point - RecyclerView notifyItemChanged(position) returns incorrect ViewHolder data

phenomenon

There are always wet shoes on the roadside.
   there is a need to refresh a single item of the list. What a simple need. After a simple CV, the code is knocked out, and then the hand is cheap for a little, and a self-test is done. Then, the tragedy appears.
   I can't remember the specific requirements. Here, simulate a similar situation. Customize a ViewHolder with a parameter count to record how many times the current ViewHolder has been refreshed, and then display it through TextView.
  layout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:viewBindingIgnore="true">

    <EditText
        android:id="@+id/index"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="number" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="onlyNotify"
        android:text="nothing Payload Refresh" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="notifyAll"
        android:text="Global refresh" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="notifyWithPayloads"
        android:text="have Payload Refresh" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/loop_rv"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        tools:context=".ui.loop.LoopActivity">
    </androidx.recyclerview.widget.RecyclerView>
</LinearLayout>

   custom ViewHolder, in which a background color is added to TextView in order to distinguish each item:

class PayloadTestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    var count = -1

    fun setText(
        holder: PayloadTestHolder,
        position: Int,
        payloads: MutableList<Any>?
    ) {
        val tv = holder.itemView.findViewWithTag<TextView>(-123456)
        count++
        tv.text = "count = $count"
        Logger.i("$position --- ${holder.hashCode()}")
        tv.setBackgroundColor(getBgColor(position))
    }

    fun getBgColor(position: Int): Int {
        return when (position % 4) {
            0 -> Color.GREEN
            1 -> Color.BLUE
            2 -> Color.RED
            3 -> Color.YELLOW
            else -> Color.GRAY
        }
    }
}

  custom adapter with itemCount of 8 is used to simulate more than one page:

private val mAdapter = object : RecyclerView.Adapter<PayloadTestHolder>() {
    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): PayloadTestHolder {
        val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300)
        val tv = TestTextView(parent.context)
        tv.layoutParams = lp
        tv.gravity = Gravity.CENTER
        tv.tag = -123456
        return PayloadTestHolder(tv)
    }
    override fun onBindViewHolder(holder: PayloadTestHolder, position: Int) {
        holder.setText(holder, position, null)
    }
    override fun onBindViewHolder(
        holder: PayloadTestHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        holder.setText(holder, position, payloads)
    }
    override fun getItemCount(): Int = 8
}  

Activity:

class RecyclerTestActivity : AppCompatActivity() {
    private val random = Random()

    private val mAdapter = object : RecyclerView.Adapter<PayloadTestHolder>() {
        override fun onCreateViewHolder(
            parent: ViewGroup,
            viewType: Int
        ): PayloadTestHolder {
            val lp = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 300)
            val tv = TestTextView(parent.context)
            tv.layoutParams = lp
            tv.gravity = Gravity.CENTER
            tv.tag = -123456
            return PayloadTestHolder(tv)
        }

        override fun onBindViewHolder(holder: PayloadTestHolder, position: Int) {
            holder.setText(holder, position, null)
        }

        override fun onBindViewHolder(
            holder: PayloadTestHolder,
            position: Int,
            payloads: MutableList<Any>
        ) {
            holder.setText(holder, position, payloads)
        }

        override fun getItemCount(): Int = 8
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_loop)
        loop_rv.layoutManager = LinearLayoutManager(this)
        loop_rv.adapter = mAdapter
        index.setText("0")
    }

    class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)

    fun randomLoop(view: View) {
        val position = random.nextInt(8)
        loop_rv.smoothScrollToPosition(position)
        Logger.d("randomLoop  ------------ $position")
    }

    fun onlyNotify(view: View) {
        mAdapter.notifyItemChanged(getIndex())
    }

    fun notifyWithPayloads(view: View) {
        mAdapter.notifyItemChanged(getIndex(), "payload")
    }

    fun notifyAll(view: android.view.View) {
        mAdapter.notifyDataSetChanged()
    }

    private fun getIndex(): Int {
        val text = index.text
        if (text.isNullOrBlank()) {
            return 0
        }
        val index = text.toString().trim().toInt()
        return if (index in 0 until 8) {
            index
        } else {
            0
        }
    }
}

   I've been struggling in the android circle for so many years. When refreshing a piece of data, I can't use notifyDataSetChanged() but notifyItemChanged(position). Of course, it's not very powerful. I know that notifyDataSetChanged() consumes resources. It's pure because when using notifyDataSetChanged() to refresh the list in current android studio, The police have called.

   the reason is, of course, due to the caching mechanism of RecyclerView (see: knyou of RecyclerView caching mechanism ), if notifyDataSetChanged() is used, all viewholders will skip all cached data and go directly to getrecycledviewpool() Get it from getrecycledview (type) and consume unnecessary resources. Run it to see the effect.

   but I found that the count increased by 1 after refreshing twice, which is obviously inconsistent with our original design intention, but there is no problem with the code logic. If it is a cache problem, I only refreshed one entry! However, in order to verify, I printed the hashCode of ViewHolder to see what was wrong

class PayloadTestHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    var count = -1

    fun setText(
        holder: PayloadTestHolder,
        position: Int,
        payloads: MutableList<Any>?
    ) {
        val tv = holder.itemView.findViewWithTag<TextView>(-123456)
        count++
        tv.text =
            "position = $position \n view code = ${tv.hashCode()}\n  holder code = ${holder.hashCode()} \n payloads = $payloads \n count = $count"
        Logger.i("$position --- ${holder.hashCode()}")
        tv.setBackgroundColor(getBgColor(position))
    }

    fun getBgColor(position: Int): Int {
        return when (position % 4) {
            0 -> Color.GREEN
            1 -> Color.BLUE
            2 -> Color.RED
            3 -> Color.YELLOW
            else -> Color.GRAY
        }
    }
}

   you can see that the hashcodes of the two holders are displayed in a circular way, that is, the count s of the two viewholders are + 1 in turn. In an instant, you look confused. Where does the more ViewHolder come from! After a breakpoint, it is found that the ViewHolder is also from getrecycledviewpool() Getrecycledview (type). The specific reason is that fat rookies can't understand it. Here's a solution.

Solution

  1. The ViewHolder caches the data and uses notifyItemChanged(position, payloads) to refresh the data. However, the limitation of this is that it can only be effective when the item is refreshed. However, when the amount of data is too large, the reuse of ViewHolder will also lead to data disorder
  2. Extract the data to the outside, use the data Bean set to hold the corresponding count value, and the change of the data value will uniformly save the final result in the data Bean of the corresponding entry, and the assignment logic will also take values from the data Bean (recommended)

proposal

Of course, the probability of the above examples in development is still very low. We all know the elegant form of data collection storage. When I used it, I should have cached the Holder's hashCode as the record label of the status. This problem also occurred. In short, it was caused by the poor understanding of the ViewHolder's cache. I hereby record it and pay attention.

Keywords: Android

Added by mazzzzz on Mon, 07 Feb 2022 09:39:32 +0200