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
- 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
- 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.