RecyclerView实现静默加载下一页 - Adapter实现

#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)。

(欢迎讨论)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值