Android知识点-RecyclerView notifyItemChanged(position)返回ViewHolder数据有误

现象

  常在路边走,哪有不湿鞋。
  这不就有一个需求,让做一个列表的单条目刷新,多简单的一个需求啊,简简单单的CV之后,代码就敲完了,然后手贱了一小下,做了个自测,然后,悲剧就这么出现了。
  具体需求我已经记不住了,这里模拟一下类似的情况,自定义一个ViewHolder,其中有一个参数count,用于记录当前ViewHolder刷新了多少次,然后通过TextView展示出来即可。
  布局:

<?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="无Payload刷新" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="notifyAll"
        android:text="全局刷新" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="notifyWithPayloads"
        android:text="有Payload刷新" />

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

  自定义ViewHolder,其中为了区分各个条目,给TextView添加了一个背景色:

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

  自定义adapter,itemCount为8是为了模拟超出一页:

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

  在android圈子里也摸爬滚打这么多年了,刷新一条数据的时候肯定不能使用notifyDataSetChanged(),而使用notifyItemChanged(position),当然,也不是能力多强知道notifyDataSetChanged()消耗资源,单纯的因为在现在的AndroidStudio中使用notifyDataSetChanged()刷新列表的时候,已经报警告了。

notifyDataSetChanged警告

  至于原因,当然是由于RecyclerView的缓存机制(参见:knyouRecyclerView缓存机制),如果使用notifyDataSetChanged(),所有的ViewHolder都会跳过所有的缓存数据,直接在getRecycledViewPool().getRecycledView(type)中获取,消耗不必要的资源,运行一下看看效果。

运行效果

  却发现刷新两次,count才增加1,明显与我们的设计初衷不符,可代码逻辑明明没有问题啊,如果说是缓存问题,可是我只刷新了一个条目啊!不过为了验证一下,还是打印了一下ViewHolder的hashCode,看看究竟出了什么问题

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

hashCode

  可以看到,竟然是两个Holder的hashCode在循环展示,也就是这两个ViewHolder的count轮流+1,,瞬间一脸懵逼,多的那个ViewHolder是哪里来的!经过一通断点,发现这个ViewHolder也是从getRecycledViewPool().getRecycledView(type)获取的。具体原因查看的层级胖菜鸟也看不懂,这里就直接给提供一下解决方案吧。

解决方案

  1. ViewHolder中缓存数据,使用notifyItemChanged(position, payloads)刷新数据,但是这样做的局限是,只能在条目刷新的时候有效,但是数据量过大时,ViewHolder的复用还会导致数据错乱
  2. 将数据提取到外部,使用数据Bean集合持有对应的count值,且数据的值的变更统一在对应条目的数据Bean中保存最终结果,且赋值逻辑也是从数据Bean中取值(推荐)

建议

当然上述的举例在开发中出现概率还是很低的,大家都知道数据集合存储的优雅形式。我当时使用的时候应该是缓存了一下Holder的hashCode作为状态的记录标签,同样出现了这个问题,总之都是对于ViewHolder的缓存理解不到位导致的,特此记录,注意就好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值