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,就知道还在加载。
完整的流程图
下面这张图,把整个分页流程串起来了。你可以对照着看,每一步对应什么代码。
避坑指南
分页看起来简单,但坑不少。我踩过的几个,分享给你:
- 重复请求——用户快速滑动时,
onScrolled会触发多次。一定要加isLoading标记。 - 数据错乱——如果接口返回顺序不对,或者用户快速翻页,可能出现数据重复或缺失。我习惯在请求时带上
page参数,保证顺序。 - 内存泄漏——
OnScrollListener记得在onDestroy时移除,或者用弱引用。 - 空数据判断——如果接口返回空列表,说明没有更多数据了。这时候应该隐藏加载提示,并且不再触发请求。
我的习惯:我会在适配器里加一个 hasMore 标记。当接口返回数据少于每页数量时,设为 false。这样滑到底部也不会再请求了。
总结
分页加载,说白了就是「监听滑动 + 请求数据 + 追加列表」。代码量不大,但细节很多。
我个人习惯把分页逻辑封装成一个 PaginationHelper 类,这样每个列表页都能复用。你如果感兴趣,可以自己试试看。
嗯,分页就聊到这里。记住一点:用户体验好不好,往往就藏在这些细节里。
公众号:蓝海资料掘金营,微信 deep3321