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

Android TV Presenter UI更新失败:无缓存崩溃/加缓存后滚动异常

问题分析与解决方法

Q1:未添加ViewHolder缓存时崩溃的原因

当RecyclerView的ViewHolder被回收或复用时,之前发起的异步请求可能还在进行,请求返回后如果直接操作该ViewHolder,此时它可能已经绑定了其他数据,甚至处于无效状态(比如视图已被销毁),必然会引发空指针或UI状态异常,导致崩溃。

Q2:添加缓存后页面自动滚动至顶部的原因

你用ViewHolder作为map的key来标记状态,但ViewHolder是RecyclerView复用池中的对象,同一个ViewHolder会被绑定不同的列表项。旧请求返回时,可能错误地更新当前复用后的ViewHolder的UI,导致列表项布局异常,触发RecyclerView重新计算布局;另外如果你的viewModel.triggerLoadRecommend请求成功后,触发了Adapter.notifyDataSetChanged()这类全局刷新,会直接让RecyclerView重置滚动位置到顶部。

具体修复方案

1. 替换ViewHolder缓存为基于列表项唯一标识的订阅管理

放弃用ViewHolder作为key,改用列表项的唯一标识(比如VideoCategoryItem的ID)来管理异步请求的生命周期,同时在更新UI前校验当前ViewHolder绑定的是否是请求对应的列表项:

class RecommendPresenter(val viewModel: HomeViewModel) : Presenter() {
    override fun onCreateViewHolder(parent: ViewGroup?): ViewHolder {
        val context = parent?.context
        val view = BaseColumnItemView(context!!)
        return ViewHolder(view)
    }

    // 用列表项ID保存对应的请求订阅,避免旧请求干扰
    private val itemDisposables = mutableMapOf<String, Disposable>()

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        // 保存当前绑定的列表项ID
        var currentItemId: String? = null
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, itemData: Any?) {
        if (itemData is VideoCategoryItem) {
            val itemId = itemData.id // 假设VideoCategoryItem有唯一id字段,根据实际调整类型
            viewHolder.currentItemId = itemId

            // 取消当前列表项的旧订阅,防止旧请求覆盖新数据
            itemDisposables.remove(itemId)?.dispose()

            val disposable = viewModel.triggerLoadRecommend(itemData.conditions)
                .subscribeOn(TScheduler.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object : ApiObserver<ArrayList<RecommendVideoResponse>>() {
                    override fun onFailed(code: Int, msg: String?) {
                        itemDisposables.remove(itemId)
                    }

                    override fun onSuccess(t: ArrayList<RecommendVideoResponse>) {
                        // 校验当前ViewHolder绑定的是否是请求对应的列表项
                        if (viewHolder.currentItemId == itemId) {
                            // 安全更新UI
                        }
                        itemDisposables.remove(itemId)
                    }
                })
            itemDisposables[itemId] = disposable
        } else {
            LOG.e("HeroBannerPresenter", "item is not VideoCategoryItem")
        }
    }

    override fun onUnbindViewHolder(viewHolder: ViewHolder) {
        // 取消当前ViewHolder绑定项的订阅,清理状态
        viewHolder.currentItemId?.let {
            itemDisposables.remove(it)?.dispose()
        }
        viewHolder.currentItemId = null
    }
}

2. 避免全局刷新,使用局部刷新更新列表

检查viewModel.triggerLoadRecommend请求成功后的数据更新逻辑,不要调用Adapter.notifyDataSetChanged(),改用notifyItemChanged(position)(如果知道对应位置)或者DiffUtil进行局部刷新。全局刷新会让RecyclerView重新初始化布局,直接回到顶部,局部刷新只会更新目标列表项,不会影响滚动位置。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 07:37:11