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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 09:06:05