You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView单条目更新时onCreateViewHolder被调用两次问题咨询

问题:RecyclerView单条目频繁更新时交替使用两个ViewHolder导致卡顿

我在使用包含频繁更新项(比如带进度条的条目)的RecyclerView,搭配ListAdapter和DiffUtils回调时发现异常:当列表只有一个条目并更新其内容时,RecyclerView会创建两个ViewHolder并交替绑定,导致UI卡顿(复杂条目时尤为明显)。

示例代码

class MainActivity : ComponentActivity() {

    data class TestData(
        val id: Long,
        val name: String
    )

    class MyHolder(view: View) : ViewHolder(view) {

        val textView = view as TextView

        fun bind(data: TestData) {
            Log.i("TAG", "bind title $data for holder $this")
            textView.text = data.name
        }
    }

    class Adapter : ListAdapter<TestData, MyHolder>(
        object : DiffUtil.ItemCallback<TestData>() {
            override fun areItemsTheSame(oldItem: TestData, newItem: TestData): Boolean {
                return oldItem.id == newItem.id
            }

            override fun areContentsTheSame(oldItem: TestData, newItem: TestData): Boolean {
                return oldItem == newItem
            }

        }
    ) {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder {
            return MyHolder(LayoutInflater.from(parent.context).inflate(R.layout.test, parent, false))
        }

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

    }

    private val myAdapter = Adapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity)

        val recycler = findViewById<RecyclerView>(R.id.recycler)
        recycler.adapter = myAdapter

        lifecycleScope.launch {

            repeat(10) {
                delay(500)
                myAdapter.submitList(listOf(TestData(1, "name $it")))
            }

        }

    }

日志输出

06-14 01:57:03.068 12096 12096 I TAG     : bind title TestData(id=1, name=name 0) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:03.586 12096 12096 I TAG     : bind title TestData(id=1, name=name 1) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:04.083 12096 12096 I TAG     : bind title TestData(id=1, name=name 2) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:04.584 12096 12096 I TAG     : bind title TestData(id=1, name=name 3) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:05.083 12096 12096 I TAG     : bind title TestData(id=1, name=name 4) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:05.582 12096 12096 I TAG     : bind title TestData(id=1, name=name 5) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:06.082 12096 12096 I TAG     : bind title TestData(id=1, name=name 6) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:06.601 12096 12096 I TAG     : bind title TestData(id=1, name=name 7) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:07.102 12096 12096 I TAG     : bind title TestData(id=1, name=name 8) for holder MyHolder{66be5bf position=0 id=-1, oldPos=-1, pLpos:-1 no parent}
06-14 01:57:07.601 12096 12096 I TAG     : bind title TestData(id=1, name=name 9) for holder MyHolder{9497c51 position=0 id=-1, oldPos=-1, pLpos:-1 no parent}

原因分析

这是ListAdapter结合RecyclerView默认动画导致的行为:

  • 每次调用submitList时,DiffUtils检测到条目内容变化,触发默认的条目变更动画(如淡入淡出)。
  • 为执行动画,RecyclerView会同时保留旧ViewHolder(展示旧内容)和新ViewHolder(展示新内容),动画完成后旧ViewHolder才会被回收。
  • 由于更新频率过高(500ms一次),前一次动画尚未结束,下一次更新已触发,导致两个ViewHolder被循环复用,无法及时回收,进而出现交替绑定的现象。

解决方案

根据场景可选择以下优化方式:

1. 关闭条目变更动画

若不需要动画效果,直接禁用RecyclerView的ItemAnimator:

recycler.itemAnimator = null

此方式会让RecyclerView直接复用现有ViewHolder,不会创建新实例,彻底解决问题。

2. 自定义ItemAnimator缩短动画时长

若需保留动画,可自定义ItemAnimator并缩短动画时间,确保动画在下次更新前完成:

class FastItemAnimator : DefaultItemAnimator() {
    override fun getChangeDuration(): Long {
        return 100 // 缩短为100ms,可按需调整
    }
}

// 应用到RecyclerView
recycler.itemAnimator = FastItemAnimator()

3. 局部更新而非全量submitList

仅更新条目部分内容时,无需每次调用submitList,可通过notifyItemChanged结合payload实现局部更新:

// Adapter中重写onBindViewHolder处理局部更新
override fun onBindViewHolder(holder: MyHolder, position: Int, payloads: MutableList<Any>) {
    if (payloads.isEmpty()) {
        super.onBindViewHolder(holder, position, payloads)
    } else {
        // 仅更新变化的内容
        val data = getItem(position)
        holder.textView.text = data.name
    }
}

// 更新逻辑
lifecycleScope.launch {
    repeat(10) {
        delay(500)
        val currentList = myAdapter.currentList.toMutableList()
        currentList[0] = currentList[0].copy(name = "name $it")
        myAdapter.submitList(currentList)
        // 或使用notifyItemChanged配合payload
        // myAdapter.notifyItemChanged(0, "UPDATE_NAME")
    }
}

此方式能减少DiffUtils计算量,避免不必要的动画触发。

4. 限制submitList的频率

若更新频率过高,可做防抖处理,合并短时间内的多次更新:

private var updateJob: Job? = null

fun updateItem(newName: String) {
    updateJob?.cancel()
    updateJob = lifecycleScope.launch {
        delay(200) // 等待200ms,无新更新再提交
        val currentList = myAdapter.currentList.toMutableList()
        currentList[0] = currentList[0].copy(name = newName)
        myAdapter.submitList(currentList)
    }
}

内容的提问来源于stack exchange,提问作者Ov3r1oad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 14:37:03