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

