#1. 前言:
App 列表页数据显示,一般一次拉取数据是10或20条,如果有更多的数据一次性拉取,需要加载数据的时间会很长,用户交互相当差,我们通常用分页处理。我们可以监听滑动实现上拉加载更多,网上也有很多的封装的 RecyclerView实现了上拉加载下一页。
静默加载下一页:就是在用户查看列表数据的时候,可以一直滑一直能查看更多的数据,让用户感觉不到应用在不断的加载下一页数据,现在要实现这样功能,比如我们一页有20条数据,我们需要在用户滑到第15条数据的时候(即倒数第5条数据)就开始加载下一页数据,这样用户滑到20条数据的时候,第二页的数据我们就已经加载好了。
直接贴上代码:
#2. 封装基础代码:
BaseLoadAdapter.kt
open class BaseLoadAdapter : RecyclerView.Adapter<BaseViewHolder>() {
companion object {
const val TYPE_ITEM = 1
const val TYPE_ITEM_LOAD_MORE = 0x00000222
}
var onItemClick: ((position: Int, action: Any) -> Unit)? = null
var onLoadMore: (() -> Unit)? = null
private var recyclerView: RecyclerView? = null
private val holderLayouts = hashMapOf<Int, HolderType>()
protected var recyclerData: ArrayList<Any> = arrayListOf()
private var loading = false //是否正在加载数据
private var loadMoreEnable = false //是否支持加载下一页
private val startLoadMoreIndex = 5 //静默开始加载下一页的时机,即倒数第几条数据开始执行加载
init {
addLoadMoreItem()
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
this.recyclerView = recyclerView
}
protected fun addItemType(
type: Int,
@LayoutRes layoutResId: Int,
holderClass: Class<*>
) {
holderLayouts[type] =
HolderType(layoutResId, holderClass)
}
override fun getItemViewType(position: Int): Int {
return if (position == recyclerData.size) {
if (getLoadMoreViewCount() == 1) {
TYPE_ITEM_LOAD_MORE
} else {
onItemViewType(position)
}
} else {
onItemViewType(position)
}
}
protected open fun onItemViewType(position: Int): Int {
return super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
val holderType = holderLayouts[viewType]!!
val view = parent.inflate(holderType.layoutId)
return createConstructorByClass(holderType.holderClass, view)
}
override fun getItemCount(): Int {
return recyclerData.size + getLoadMoreViewCount()
}
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
autoLoadMore(position)
holder.bindData(position, getItemData(position), onItemClick)
}
private fun getItemData(position: Int): Any? {
return if (position == recyclerData.size && getLoadMoreViewCount() == 1) {
null
} else {
recyclerData[position]
}
}
fun setRecyclerData(data: List<Any>) {
recyclerData.clear()
recyclerData.addAll(data)
notifyDataSetChanged()
}
fun addRecyclerData(data: List<Any>) {
recyclerData.addAll(data)
notifyItemRangeInserted(recyclerData.size - data.size, data.size)
loadMoreComplete()
}
private fun autoLoadMore(position: Int) {
if (itemCount < startLoadMoreIndex) {
return
}
if (getLoadMoreViewCount() == 0) {
return
}
//Load "startLoadMoreIndex" positions in advance
if (position < itemCount - getLoadMoreViewCount() - startLoadMoreIndex) {
return
}
if (!loading) {
loading = true
recyclerView?.apply {
post { onLoadMore?.invoke() }
} ?: run {
onLoadMore?.invoke()
}
}
}
fun setLoadMoreEnable(isLoadMore: Boolean) {
loadMoreEnable = isLoadMore
}
private fun addLoadMoreItem() {
addItemType(TYPE_ITEM_LOAD_MORE, R.layout.item_loading_progress, LoadingHolder::class.java)
}
private fun loadMoreComplete() {
loading = false
if (getLoadMoreViewCount() > 0) {
notifyItemChanged(recyclerData.size)
}
}
private fun <T> createConstructorByClass(
clz: Class<T>,
view: View
): BaseViewHolder {
val create = clz.getDeclaredConstructor(View::class.java).apply {
isAccessible = true
}
return create.newInstance(view) as BaseViewHolder
}
private fun getLoadMoreViewCount(): Int {
if (onLoadMore == null || !loadMoreEnable) {
return 0
}
if (holderLayouts[TYPE_ITEM_LOAD_MORE] == null) {
return 0
}
return if (recyclerData.isEmpty()) {
0
} else 1
}
}
class LoadingHolder(itemView: View) : BaseViewHolder(itemView) {
override fun bindData(
position: Int,
item: Any?,
onItemClick: ((position: Int, action: Any) -> Unit)?
) {
}
}
data class HolderType(
@LayoutRes val layoutId: Int,
val holderClass: Class<*>
)
备注:实现自定义静默加载下一页的Adapter基础类。
⚠️ 这里需要item_loading_progress布局文件是为了避免用户快速滑动,而网络延迟又高,会出现滑动到底部无响应(其实是在等待数据加载回来),这里在底部添加该布局,是用来优化体验告诉用户还在加载数据。
item_loading_progress.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/pb_loading"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="20dp"
android:progressTint="@color/colorMain"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
BaseViewHolder.kt
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view) {
abstract fun bindData(
position: Int,
item: Any?,
onItemClick: ((position: Int, action: Any) -> Unit)?
)
}
#3. 用法:
DemoAdapter.kt
class DemoAdapter : BaseLoadAdapter() {
companion object {
private const val TAG = "DemoAdapter"
}
init {
addItemType(TYPE_ITEM, R.layout.item_demo, DemoViewHolder::class.java)
}
override fun onItemViewType(position: Int): Int {
return TYPE_ITEM
}
// 设置列表数据(首页数据)
fun setData(data: List<String>) {
setRecyclerData(data)
}
// 加载下一页数据
fun addData(newData: List<String>) {
addRecyclerData(newData)
}
}
DemoViewHolder:
class DemoViewHolder(itemView: View) : BaseViewHolder(itemView) {
override fun bindData(
position: Int,
item: Any?,
onItemClick: ((position: Int, action: Any) -> Unit)?
) {
with(itemView) {
if (item != null && item is String) {
tv.text = item
// 为ViewHolder的视图定义点击监听器
setOnClickListener {
onItemClick?.invoke(position, item)
}
}
}
}
}
在Activity中,如下示例:
private val demoAdapter: DemoAdapter by lazy { DemoAdapter() }
override fun onCreate(savedInstanceState: Bundle?) {
// ... 省略部分代码
rv.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
rv.adapter = walletLogsAdapter
demoAdapter.onLoadMore = {
loadNextPage()
}
demoAdapter.addData(list)
demoAdapter.setLoadMoreEnable(page < totalPage)
}
如需查看更详细代码,请转至Git地址:MVVM-Project-Hilt
补充说明:因为不推荐使用“kotlin-android-extensions”Gradle 插件了,所以该架构已经替换使用视图绑定(ViewBinding)。
(欢迎讨论)