Kotlin MVVM架构下RecyclerView分页加载数据重复问题求助
Kotlin MVVM实现RecyclerView分页数据重复问题解决方案
核心问题根源
- 每次触发分页请求时都重复注册LiveData观察者:普通LiveData存在粘性特性,新注册的观察者会收到LiveData中已存储的旧数据,导致同一批数据被多次添加到列表中
- 分页状态逻辑异常:pageNumber递增时机、loading状态重置逻辑存在问题,可能导致同一页数据被多次请求
- LiveData未做单次消费处理:配置变更、视图重建时LiveData会再次分发已有数据,造成重复添加
具体修复方案
1. 调整ViewModel初始化和观察者注册逻辑
将ViewModel实例化、LiveData观察者注册逻辑移到onCreate方法中全局执行一次,不要放在分页请求方法中重复调用:
// SearchResultActivity.kt的onCreate方法修改 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initialize() // 全局仅初始化一次ViewModel,仅注册一次观察者 val myViewModel = ViewModelProviders.of(this).get(PullVM::class.java) // 注册结束标识监听 myViewModel.endofList.observe(this, { if (it == true) { isEnd = true progressBar.visibility = View.GONE Toast.makeText(this@SearchResultActivity, "所有PR已加载完成", Toast.LENGTH_SHORT).show() } }) // 注册请求状态监听 myViewModel.status.observe(this, { if (it == false) { showError(getString(R.string.no_net)) // 请求失败重置loading状态,允许重试 loading = false } }) // 注册列表数据监听 myViewModel.getPullDataFromVM().observe(this, { if (it != null) { listRepos(it) } else { showError(getString(R.string.nothing_found)) loading = false } }) // 首次请求数据 getDataPull(userName, repoName) loadNextData() }
对应修改getDataPull方法,仅保留请求触发逻辑:
private fun getDataPull(username: String?, reponame: String?) { // 已到最后一页直接拦截请求 if (isEnd) return progressBar.visibility = View.VISIBLE val myViewModel = ViewModelProviders.of(this).get(PullVM::class.java) myViewModel.getPullList(username.toString(), reponame.toString(), pageNumber) }
2. 使用单次消费的LiveData避免粘性事件
自定义SingleLiveEvent替换原生MutableLiveData,确保每个数据仅被消费一次:
class SingleLiveEvent<T> : MutableLiveData<T>() { private val pending = AtomicBoolean(false) override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { super.observe(owner) { t -> if (pending.compareAndSet(true, false)) { observer.onChanged(t) } } } override fun setValue(t: T?) { pending.set(true) super.setValue(t) } override fun postValue(t: T?) { pending.set(true) super.postValue(t) } }
修改ViewModel中的LiveData类型:
class PullVM : ViewModel() { // 替换为SingleLiveEvent var pullList: SingleLiveEvent<List<RepoPull>> var status = MutableLiveData<Boolean?>() var endofList = MutableLiveData<Boolean?>() init { pullList = SingleLiveEvent() } // 其余逻辑保持不变 }
3. 优化分页逻辑和列表更新逻辑
调整滚动监听边界判断、pageNumber递增时机,增加兜底去重逻辑,替换全局刷新为DiffUtil局部更新:
// 滚动监听逻辑优化 private fun loadNextData() { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) val mLayoutManger = recyclerView.layoutManager as LinearLayoutManager visibleItemsCount = mLayoutManger.childCount totalItemsCount = mLayoutManger.itemCount firstVisibleItemsCount = mLayoutManger.findFirstVisibleItemPosition() // 增加未到最后一页的判断,提前5个item触发预加载 if (!loading && !isEnd && (firstVisibleItemsCount + visibleItemsCount) >= totalItemsCount - 5) { getDataPull(userName, repoName) loading = true Log.d("PAGE", pageNumber.toString()) } } }) } // 列表数据更新逻辑优化 fun listRepos(repos: List<RepoPull>) { if (!isEnd && repos.isNotEmpty()) { progressBar.visibility = View.GONE // 兜底去重逻辑,假设RepoPull的id为唯一标识 val newRepos = repos.filter { newRepo -> fillPullList.none { it.id == newRepo.id } } val oldSize = fillPullList.size fillPullList.addAll(newRepos) // 使用DiffUtil局部更新 val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun getOldListSize() = oldSize override fun getNewListSize() = fillPullList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return fillPullList[oldItemPosition].id == fillPullList[newItemPosition].id } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return fillPullList[oldItemPosition] == fillPullList[newItemPosition] } }) diffResult.dispatchUpdatesTo(pullAdapter) // 请求成功后再更新页码和分页状态 pageNumber++ loading = false previousTotal = fillPullList.size } }
内容的提问来源于stack exchange,提问作者Xay
相关产品推荐
相关产品推荐

