21、加载更多与分页:监听滑动到底部,加载下一页数据

分页加载,说白了就是「数据太多,一次拿不完」。

我刚开始做列表页的时候,总觉得把数据全查出来扔给 RecyclerView 就完事了。直到有一次,后台返回了 2000 条数据,页面直接卡死……嗯,从那以后,我再也不敢小看分页了。

分页的核心思路

分页其实就三件事:

  • 监听滑动位置——什么时候该加载下一页?
  • 请求下一页数据——调用接口,拿到新数据
  • 追加到列表——把新数据插到原有数据后面

你想想看,这三步缺一不可。少了哪一步,分页都跑不起来。

核心要点:分页不是「一次性加载」,而是「按需加载」。用户滑到底部,才触发下一次请求。

监听滑动到底部

RecyclerView 自带了一个监听器,叫 OnScrollListener。我们可以重写它的 onScrolled 方法,来判断是否滑到了底部。

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)

        val layoutManager = recyclerView.layoutManager as LinearLayoutManager
        val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
        val totalItemCount = layoutManager.itemCount

        // 判断是否滑到底部
        if (lastVisibleItem == totalItemCount - 1) {
            // 加载下一页
            loadMoreData()
        }
    }
})

这里有个细节:findLastVisibleItemPosition() 返回的是当前屏幕上最后一个可见 item 的位置。如果这个位置等于总 item 数减 1,说明已经滑到底了。

小提示:我习惯在判断底部之前,加一个「是否正在加载」的标记。防止用户快速滑动时,重复触发多次请求。

加载更多数据

触发加载之后,我们需要调用接口,拿到下一页的数据。这里我一般会维护一个 page 变量,每次请求成功后自增。

private var currentPage = 1
private var isLoading = false

private fun loadMoreData() {
    if (isLoading) return
    isLoading = true

    // 显示底部加载中的提示
    adapter.showLoading()

    viewModelScope.launch {
        val result = repository.getItems(page = currentPage)
        result.onSuccess { items ->
            adapter.addItems(items)
            currentPage++
            isLoading = false
            adapter.hideLoading()
        }.onFailure { error ->
            // 处理错误
            isLoading = false
            adapter.showError(error.message)
        }
    }
}

你看,逻辑其实很简单。但我在项目中遇到过一个问题:如果接口返回的数据少于每页数量,说明没有更多数据了。这时候应该隐藏加载提示,不再触发请求。

注意:不要忘记处理「没有更多数据」的情况。否则用户滑到底部,会一直显示加载中,体验很差。

适配器的配合

适配器这边,需要支持两种 item 类型:正常的数据 item 和底部的加载 item。我一般用 getItemViewType 来区分。

override fun getItemViewType(position: Int): Int {
    return if (position == itemCount - 1 && isLoading) {
        VIEW_TYPE_LOADING
    } else {
        VIEW_TYPE_ITEM
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return when (viewType) {
        VIEW_TYPE_LOADING -> LoadingViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.item_loading, parent, false)
        )
        else -> ItemViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.item_data, parent, false)
        )
    }
}

这样做的好处是,加载状态和正常数据互不干扰。用户看到底部有个转圈圈的 loading,就知道还在加载。

完整的流程图

下面这张图,把整个分页流程串起来了。你可以对照着看,每一步对应什么代码。

分页加载流程图 用户滑动列表 判断是否滑到底部 lastVisibleItem == totalItemCount - 1 是否正在加载? isLoading == true ? 调用接口加载下一页 page++,追加数据 更新列表 关键变量 currentPage:当前页码 isLoading:是否正在加载 lastVisibleItem:最后可见位置 totalItemCount:总 item 数 注意事项 • 防止重复请求 • 处理无更多数据 • 错误状态恢复 • 加载提示显示/隐藏

避坑指南

分页看起来简单,但坑不少。我踩过的几个,分享给你:

  • 重复请求——用户快速滑动时,onScrolled 会触发多次。一定要加 isLoading 标记。
  • 数据错乱——如果接口返回顺序不对,或者用户快速翻页,可能出现数据重复或缺失。我习惯在请求时带上 page 参数,保证顺序。
  • 内存泄漏——OnScrollListener 记得在 onDestroy 时移除,或者用弱引用。
  • 空数据判断——如果接口返回空列表,说明没有更多数据了。这时候应该隐藏加载提示,并且不再触发请求。

我的习惯:我会在适配器里加一个 hasMore 标记。当接口返回数据少于每页数量时,设为 false。这样滑到底部也不会再请求了。

总结

分页加载,说白了就是「监听滑动 + 请求数据 + 追加列表」。代码量不大,但细节很多。

我个人习惯把分页逻辑封装成一个 PaginationHelper 类,这样每个列表页都能复用。你如果感兴趣,可以自己试试看。

嗯,分页就聊到这里。记住一点:用户体验好不好,往往就藏在这些细节里。


公众号:蓝海资料掘金营,微信 deep3321