Android Paging3分页+ConcatAdapter+空数据视图+下拉刷新(SwipeRefreshLayout)+加载更多+错误重试 (示例)

这篇具有很好参考价值的文章主要介绍了Android Paging3分页+ConcatAdapter+空数据视图+下拉刷新(SwipeRefreshLayout)+加载更多+错误重试 (示例)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

android paging3,Android,Android Jetpack,android,分页加载

引入库

implementation 'androidx.paging:paging-runtime-ktx:3.1.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.recyclerview:recyclerview:1.3.0'

paging 库,目前还是有点小bug ,后面说


数据模型定义

// 分页请求的数据响应,至少要返回一个总数量; page、pageIndex 都可本地定义,然后 可以计算出当前请求响应后,历史返回的总数量;最终计算出是否还有下一页
data class CustomerPageData(val totalCount: Int, val data: List<CustomerData>)

data class CustomerData(val id: String, val name: String)

分页 adapter

import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.common.BaseViewHolder
import com.stone.stoneviewskt.data.CustomerData
import com.stone.stoneviewskt.util.logi

/**
 * desc:
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/10 11:20
 */
class PageListAdapter: PagingDataAdapter<CustomerData, BaseViewHolder>(object : DiffUtil.ItemCallback<CustomerData>() {

    // 是否是同一条 item
    override fun areItemsTheSame(oldItem: CustomerData, newItem: CustomerData): Boolean {
        logi("areItemsTheSame")
        return oldItem.id == newItem.id
    }

    // 是否内容相同
    override fun areContentsTheSame(oldItem: CustomerData, newItem: CustomerData): Boolean {
        logi("areContentsTheSame")
        return oldItem.id == newItem.id
    }
}) {

    // item 点击事件
    private var mBlock: ((position: Int, data: CustomerData) -> Unit)? = null

    override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
        val data = getItem(position) ?: return
        holder.findView<TextView>(R.id.tv_name).text = data.name

        holder.itemView.setOnClickListener {
            mBlock?.invoke(position, data)
        }

        if (position % 2 == 0) {
            holder.itemView.setBackgroundColor(Color.parseColor("#c0ff00ff"))
        } else {
            holder.itemView.setBackgroundColor(Color.parseColor("#c0abc777"))
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_page_list, parent, false)
        return BaseViewHolder(itemView)
    }

    fun setItemClick(block: (position: Int, data: CustomerData) -> Unit) {
        this.mBlock = block
    }

}

布局文件就是显示两个TextView,就不贴了


加载更多 adapter

布局文件:

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="10dp"
    android:paddingBottom="10dp">

    <ProgressBar
        android:id="@+id/progressBar"
        style="@style/myProgressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminateBehavior="repeat"
        android:indeterminateDuration="700"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/tv_load"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_load"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingStart="20dp"
        android:paddingEnd="20dp"
        android:text="数据加载中..."
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/progressBar"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_all_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingStart="20dp"
        android:paddingEnd="20dp"
        android:text="已加载全部数据"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_retry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:paddingStart="20dp"
        android:paddingEnd="20dp"
        android:text="重试"
        android:textSize="18sp"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

adapter 实现:

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.common.BaseViewHolder
import com.stone.stoneviewskt.util.logi

/**
 * desc:    加载更多
 * author:  stone
 * email:   aa86799@163.com
 * time:    2023/6/8 16:11
 */
class LoadMoreAdapter(private val retryBlock: () -> Unit) : LoadStateAdapter<BaseViewHolder>() {
    override fun onBindViewHolder(holder: BaseViewHolder, loadState: LoadState) {
        when (loadState) {
            is LoadState.NotLoading -> { // 非加载中
                logi("LoadMoreAdapter---onBindViewHolder---LoadState.NotLoading")
                holder.itemView.findViewById<ProgressBar>(R.id.progressBar).visibility = View.GONE
                holder.itemView.findViewById<TextView>(R.id.tv_load).visibility = View.GONE

                holder.itemView.findViewById<TextView>(R.id.tv_all_message).visibility = View.VISIBLE

                holder.itemView.findViewById<TextView>(R.id.btn_retry).visibility = View.GONE
            }

            is LoadState.Loading -> { // 加载中
                holder.itemView.findViewById<ProgressBar>(R.id.progressBar).visibility = View.VISIBLE
                holder.itemView.findViewById<TextView>(R.id.tv_load).visibility = View.VISIBLE

                holder.itemView.findViewById<TextView>(R.id.tv_all_message).visibility = View.GONE

                holder.itemView.findViewById<TextView>(R.id.btn_retry).visibility = View.GONE
            }

            is LoadState.Error -> {
                holder.itemView.findViewById<ProgressBar>(R.id.progressBar).visibility = View.GONE
                holder.itemView.findViewById<TextView>(R.id.tv_load).visibility = View.GONE

                holder.itemView.findViewById<TextView>(R.id.tv_all_message).visibility = View.GONE

                val btnRetry = holder.itemView.findViewById<TextView>(R.id.btn_retry)
                btnRetry.visibility = View.VISIBLE
                btnRetry.text = "发生了错误:${loadState.error.message},点击重试"
                btnRetry.setOnClickListener {
                    retryBlock()
                }
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): BaseViewHolder {
        val itemView = LayoutInflater.from(parent.context).inflate(R.layout.layout_load_more, parent, false)
        return BaseViewHolder(itemView)
    }

    // 是否作为 item 显示
    override fun displayLoadStateAsItem(loadState: LoadState): Boolean {
        logi("loadState -- $loadState")
        return loadState is LoadState.Loading
                || loadState is LoadState.Error
                || (loadState is LoadState.NotLoading && loadState.endOfPaginationReached)
        // loadState.endOfPaginationReached false 表示有更多的数据要加载
    }

}

adapter 继承自 LoadStateAdapter,关联的数据对象 LoadState 有三个状态。

  • LoadState.NotLoading 表示 非加载中,即加载完成时的状态。如果 displayLoadStateAsItem()中的实现是|| (loadState is LoadState.NotLoading) 那通过日志会发现,在初次加载分页数据时,会发生一次 LoadState.NotLoading,在加载完成所有数据后,每次滑动到底部时,都会发生一次 LoadState.NotLoading。而改成如上实现 || (loadState is LoadState.NotLoading && loadState.endOfPaginationReached) 则初次加载分页数据时,不会发生 LoadState.NotLoading 。
    所以,在 onBindViewHolder() 中的实现是,NotLoading 时,显示加载完成文本框
  • LoadState.Loading 表示 加载中的状态。显示 ProgressBar和一个文本消息框
  • LoadState.Error 表示 加载错误的状态。其关联的是 PagingSource#load() 返回的 LoadResult.Error 结果。
    显示 发生了错误:${loadState.error.message},点击重试 的文本框

displayLoadStateAsItem() 通过返回值判断 是否作为 item 显示。前面说 loadState is LoadState.NotLoading && loadState.endOfPaginationReached 非加载中&& 加载完成所有数据时 LoadState.NotLoading 状态才显示,即内部调用bindViewHolder()


空数据 adapter

import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.adapter.BaseRvAdapter
import com.stone.stoneviewskt.common.BaseViewHolder

/**
 * desc:    空数据
 * author:  stone
 * email:   aa86799@163.com
 * time:    2023/6/9 10:45
 */
class EmptyAdapter: BaseRvAdapter<String>(R.layout.layout_empty) {

    override fun fillData(holder: BaseViewHolder, position: Int, data: String) {

    }
}

布局文件 里就一个文本框,text=“暂无数据”


分页数据源

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.stone.stoneviewskt.BuildConfig
import com.stone.stoneviewskt.data.CustomerData
import com.stone.stoneviewskt.data.CustomerPageData
import com.stone.stoneviewskt.util.logi
import com.stone.stoneviewskt.util.showShort
import kotlin.random.Random

/**
 * desc:    分页数据源
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/10 11:26
 */
class PageListPageSource : PagingSource<Int, CustomerData>() {

    private var flagA = false
    private var flagB = false
    companion object {
        private var mockEmpty = true // 静态字段 模拟空数据
    }

    override fun getRefreshKey(state: PagingState<Int, CustomerData>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CustomerData> {
        val currentPage = params.key ?: 1 // 当前页码
        val pageSize = params.loadSize // 每页多少条数据
        var bean: CustomerPageData? = null // 分页数据
        kotlin.runCatching { // 异常发生后,执行 onFailure {}
            // todo 真实开发时,移除模拟错误代码

            // 模拟 第2页 一定抛异常
            if (currentPage == 2 && !flagB) {
                flagA = true
            }
            if (flagA) {
                flagA = false
                flagB = true
                throw IllegalStateException("test-error")
            }


            if (currentPage % 2 == 0 && Random.nextInt() % 2 == 0) { // 当偶数页时,随机发生异常。  注意异常发生后,对请求的页码 -- currentPage是否有影响
                throw IllegalStateException("test-error")
            }
//            Api.createRetrofit(CustomerApiService::class.java).customerList(currentPage,pageSize) // 真实网络请求方式
            mockData(currentPage, pageSize)// todo 真实开发时,移除模拟数据代码
        }.onSuccess {
            bean = it
        }.onFailure {
            it.printStackTrace()
            // 当数据加载中时,销毁了界面(比如--回退到上一页),就有很大概率触发 CancellationException
            // 跳过该异常
            if (it !is java.util.concurrent.CancellationException) {
                showShort("出错了" + it.message)
                return LoadResult.Error(it) // 如果返回 error,那后续 加载更多(即底部上拉)将不会触发
            }
        }
        val prevKey = if (currentPage > 1) currentPage - 1 else null // 当前页不是第一页的话,前一页为当前页减一
        val hasMore = currentPage * pageSize < (bean?.totalCount ?: 0)
        val nextKey = if (hasMore) currentPage + 1 else null // 当前页有数据的话,下一页就是,当前页加一,反之为空
        logi("currentPage:$currentPage    pageSize:$pageSize    prevKey:$prevKey     nextKey:$nextKey")
        return try {
            LoadResult.Page(
                data = bean?.data ?: arrayListOf(), // 数据
                prevKey = prevKey, // 为当前页的前一页
                nextKey = nextKey  // 为当前页的下一页
            )
        } catch (e: Exception) {
            e.printStackTrace()
            return LoadResult.Error(e)
        }
    }

    // 模拟分页数据
    private fun mockData(pageIndex: Int, pageSize: Int): CustomerPageData? {
        if (mockEmpty) {
            mockEmpty = false
            return null
        }
        logi("分页参数:[pageIndex-$pageIndex] [pageSize-$pageSize]")
        val list = arrayListOf<CustomerData>()
        val totalCount = 55
        val currentCount = if (totalCount > pageIndex * pageSize) pageSize else totalCount - (pageIndex - 1) * pageSize
         (1..currentCount).forEach {
             val currentPosition = (pageIndex - 1) * pageSize + it
             list.add(CustomerData("id-$currentPosition", "name-$currentPosition"))
        }
        return CustomerPageData(totalCount, list)
    }
}

上面的实现中,有一些模拟数据和加载数据出错的代码,都有注释


ViewModel 提供加载数据源的方法

/**
 * desc:
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/10 12:12
 */
class PageListViewModel : ViewModel() {

    // todo 截止到 3.1.1版本,官方库有bug:超出 initialLoadSize 所在页码后, 再retry 没问题;反之retry,会数据重复
    // todo 为了规避 建议 真实开发时,initialLoadSize 和 pageSize 的值相同。 或看看后续版本是否有修复

    fun loadData(): Flow<PagingData<CustomerData>> {
        return Pager(
            config = PagingConfig(
                pageSize = 10,
                initialLoadSize = 15, // 初始取的 pageSize,通常要大于指定的 pageSize   但大于了,在retry时又会出现bug
                prefetchDistance = 1   // 滑动到底部的 prefetchDistance 条数据 时自动加载下一页(如果还有下一页)
            ),
            // 每页都会调用一次 pagingSource#load(), 请求数据
            pagingSourceFactory = { PageListPageSource() }
        ).flow.cachedIn(viewModelScope) // 设置缓存
    }
}

因为 pagingSource 加载的bug,导致对应情形下的数据重复。想用 flow.distinctUntilChangedBy 操作符来去重,
可是 该flow类型是 Flow<PagingData<CustomerData>> 而不是 Flow<CustomerData> ,所以然并卵…

结合以上实现的 Fragment

import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.ConcatAdapter
import com.stone.stoneviewskt.R
import com.stone.stoneviewskt.common.BaseBindFragment
import com.stone.stoneviewskt.databinding.FragmentPageListBinding
import com.stone.stoneviewskt.util.logi
import com.stone.stoneviewskt.util.showShort
import kotlinx.coroutines.flow.collectLatest

/**
 * desc:    Paging3分页+ConcatAdapter+空数据视图+下拉刷新(SwipeRefreshLayout)+加载更多+错误重试
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/10 11:09
 */
class PageListFragment: BaseBindFragment<FragmentPageListBinding>(R.layout.fragment_page_list) {

    private val viewModel by lazy { ViewModelProvider(this)[PageListViewModel::class.java] }
    private lateinit var concatAdapter: ConcatAdapter // 可合并 adapter
    private val adapter by lazy { PageListAdapter() }
    private val loadMoreAdapter by lazy {
        LoadMoreAdapter { // 点击 重试按钮
            adapter.retry()
        }
    }
    private val emptyAdapter: EmptyAdapter by lazy { EmptyAdapter() }

    override fun onPreparedView(savedInstanceState: Bundle?) {
        super.onPreparedView(savedInstanceState)

        concatAdapter = adapter.withLoadStateFooter(loadMoreAdapter) // 将 loadMoreAdapter 添加为 footer 效果
        emptyAdapter.updateData(listOf("")) // 向 emptyAdapter 添加一项,当其显示时,只显示一个对应视图
        mBind.rvPage.adapter = concatAdapter // 这时 已 添加了 loadMoreAdapter

        // 设置加载状态监听
        adapter.addLoadStateListener {
            logi("监听到loadState -- $it")
            // 如果 状态是 未加载(加载完成也算) 或 发生错误
            if (it.refresh is LoadState.NotLoading) { // 见下文分析
                mBind.swipeRefresh.isRefreshing = false
                if (adapter.itemCount == 0) { // 真实数据为空
                    if (concatAdapter.adapters.contains(loadMoreAdapter)) {
                        concatAdapter.removeAdapter(loadMoreAdapter) // 移除加载更多
                        logi("removeAdapter -- loadMoreAdapter")
                    }
                    if (!concatAdapter.adapters.contains(emptyAdapter)) {
                        concatAdapter.addAdapter(emptyAdapter) // 添加空数据视图   仅在未添加过时才添加
                    }
                } else {
                    if (concatAdapter.adapters.contains(emptyAdapter)) {
                        concatAdapter.removeAdapter(emptyAdapter)
                        logi("removeAdapter -- emptyAdapter")
                        concatAdapter.addAdapter(loadMoreAdapter) // 只添加一次 loadMore
                    }
                }

            } else if (it.refresh is LoadState.Error) {
                mBind.swipeRefresh.isRefreshing = false
            }
        }

        adapter.setItemClick { position, data ->
            showShort("点击查看 --- ${data.name}")
        }

        mBind.swipeRefresh.setOnRefreshListener {
            loadData()
        }

        mBind.swipeRefresh.isRefreshing = true
        loadData()
    }

    private fun loadData() {
        lifecycleScope.launchWhenResumed {
            viewModel.loadData().collectLatest { // 加载数据
                adapter.submitData(it) // 绑定分页数据源
            }
        }
    }
}
  • 绑定分页数据源的关键代码是 adapter.submitData(data)
  • 分页数据源加载数据出错后,重试的关键代码是 adapter.retry()
  • adapter.withLoadStateFooter(loadMoreAdapter)返回的是 ConcatAdapter (可合并adapter)。
    loadMoreAdapter 添加为 footer。通过 ConcatAdapter ,在数据为空时,添加 EmptyAdapter
  • addLoadStateListener 状态监听,通过日志发现:

LoadState.NotLoading 每次数据加载开始和结束时都会发生。
当数据真实数据项为0时,如果有 加载更多视图 先移除,如果未 添加空数据视图,添加空数据视图。
当数据真实数据项为非0时,如果有 添加空数据视图,则移除空数据视图,并添加 加载更多视图。初始时,通过 concatAdapter = adapter.withLoadStateFooter(loadMoreAdapter) 已添加过 加载更多视图。
如上,不会造成重复添加 空数据视图 或 加载更多视图
注意:这里的 NotLoading 触发时机 和 LoadMoreAdapter#displayLoadStateAsItem() 相应的实现没有关系


数据重复问题

截止到 3.1.1版本,官方库有bug:超出 initialLoadSize 所在页码后, 再retry 没问题;反之retry,会数据重复。比如 pageSize=10,initialLoadSize=15,初始加载15个数据后,当前页码对应 pageSize=10 的情形下是 2;这时再请求第2页,会把第二页的10个条目加载进来并显示,这就发生了数据重复。

为了规避该问题,建议 真实开发时,initialLoadSize 和 pageSize 的值相同。 或看看后续版本是否有修复


对 PagingSource 进行封装

定义好统一的 PageData 的数据字段格式后,不同的业务 都实现一次 PagingSource ,会有大量的重复的代码。因此需要封装一下

base 分页数据类

open class BasePageData<T>(open val totalCount: Int, val data: List<T>)

base 分页数据源

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.stone.stoneviewskt.BuildConfig
import com.stone.stoneviewskt.data.BasePageData
import com.stone.stoneviewskt.util.logi
import com.stone.stoneviewskt.util.showShort

/**
 * desc:    base 分页数据源
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/11 13:05
 */
abstract class BasePageSource<T: Any> : PagingSource<Int, T>() {


    override fun getRefreshKey(state: PagingState<Int, T>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    abstract fun loadData(pageIndex: Int, pageSize: Int): BasePageData<T>?

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> {
        val currentPage = params.key ?: 1 // 当前页码
        val pageSize = params.loadSize // 每页多少条数据
        var bean: BasePageData<T>? = null // 分页数据
        kotlin.runCatching { // 异常发生后,执行 onFailure {}
            loadData(currentPage, pageSize)
        }.onSuccess {
            bean = it
        }.onFailure {
            it.printStackTrace()
            // 当数据加载中时,销毁了界面(比如--回退到上一页),就有很大概率触发 CancellationException
            // 跳过该异常
            if (it !is java.util.concurrent.CancellationException) {
                showShort("出错了" + it.message)
                return LoadResult.Error(it) // 如果返回 error,那后续 加载更多(即底部上拉)将不会触发
            }
        }
        val prevKey = if (currentPage > 1) currentPage - 1 else null // 当前页不是第一页的话,前一页为当前页减一
        val hasMore = currentPage * pageSize < (bean?.totalCount ?: 0)
        val nextKey = if (hasMore) currentPage + 1 else null // 当前页有数据的话,下一页就是,当前页加一,反之为空
        logi("currentPage:$currentPage    pageSize:$pageSize    prevKey:$prevKey     nextKey:$nextKey")
        return try {
            LoadResult.Page(
                data = bean?.data ?: arrayListOf(), // 数据
                prevKey = prevKey, // 为当前页的前一页
                nextKey = nextKey  // 为当前页的下一页
            )
        } catch (e: Exception) {
            e.printStackTrace()
            return LoadResult.Error(e)
        }
    }
}

具体的数据源实现

import com.stone.stoneviewskt.data.BasePageData
import com.stone.stoneviewskt.data.CustomerData
import com.stone.stoneviewskt.util.logi

/**
 * desc:    分页数据源
 * author:  stone
 * email:   aa86799@163.com
 * blog :   https://stone.blog.csdn.net
 * time:    2023/6/11 13:11
 */
class PageListPageSource2 : BasePageSource<CustomerData>() {

    override fun loadData(pageIndex: Int, pageSize: Int): BasePageData<CustomerData>? {
        return mockData(pageIndex, pageSize)
    }

    // 模拟分页数据
    private fun mockData(pageIndex: Int, pageSize: Int): BasePageData<CustomerData> {
        logi("分页参数:[pageIndex-$pageIndex] [pageSize-$pageSize]")
        val list = arrayListOf<CustomerData>()
        val totalCount = 55
        val currentCount = if (totalCount > pageIndex * pageSize) pageSize else totalCount - (pageIndex - 1) * pageSize
         (1..currentCount).forEach {
             val currentPosition = (pageIndex - 1) * pageSize + it
             list.add(CustomerData("id-$currentPosition", "name-$currentPosition"))
        }
        return BasePageData(totalCount, list)
    }
}

欧了,松口气 ~~


# 对 Adapter 的管理,进行封装 (更新于 230705)

发现,在 UI 类 中应用时,总是要定义这几个adapter、进行初始化、设置加载状态监听等。
基于这些重复动作,所以需要进行封装,有了如下:

import androidx.paging.LoadState
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.ConcatAdapter
import com.stone.stoneviewskt.common.BaseViewHolder
import com.stone.stoneviewskt.util.logi

/**
 * desc:    分页 Adapter-UI 辅助类
 * author:  stone
 * email:   aa86799@163.com
 * time:    2023/7/5 09:20
 */
abstract class PageUiHelper<T : Any>(private val adapter: PagingDataAdapter<T, BaseViewHolder>) {

    private lateinit var concatAdapter: ConcatAdapter // 可合并 adapter
    val loadMoreAdapter by lazy {
        LoadMoreAdapter { // 点击 重试按钮
            adapter.retry()
        }
    }
    val emptyAdapter: EmptyAdapter by lazy { EmptyAdapter() }
	
	abstract fun onNotLoadingOrErrorState()
	
    fun init() {
        concatAdapter = adapter.withLoadStateFooter(loadMoreAdapter) // 将 loadMoreAdapter 添加为 footer 效果
        emptyAdapter.updateData(listOf("")) // 向 emptyAdapter 添加一项,当其显示时,只显示一个对应视图
        
        // 设置加载状态监听
        adapter.addLoadStateListener {
            logi("监听到loadState -- $it")
            // 如果 状态是 未加载(加载完成也算) 或 发生错误
            if (it.refresh is LoadState.NotLoading) {
                onNotLoadingOrErrorState()
                if (adapter.itemCount == 0) { // 真实数据为空
                    if (concatAdapter.adapters.contains(loadMoreAdapter)) {
                        concatAdapter.removeAdapter(loadMoreAdapter) // 移除加载更多
                        logi("removeAdapter -- loadMoreAdapter")
                    }
                    if (!concatAdapter.adapters.contains(emptyAdapter)) {
                        concatAdapter.addAdapter(emptyAdapter) // 添加空数据视图   仅在未添加过时才添加
                    }
                } else {
                    if (concatAdapter.adapters.contains(emptyAdapter)) {
                        concatAdapter.removeAdapter(emptyAdapter)
                        logi("removeAdapter -- emptyAdapter")
                        concatAdapter.addAdapter(loadMoreAdapter) // 只添加一次 loadMore
                    }
                }

            } else if (it.refresh is LoadState.Error) {
                onNotLoadingOrErrorState()
            }
        }
    }

    fun getMainAdapter() = concatAdapter
}

ViewPager2 中使用发现的 bug(更新于 230719)

TabLayout + ViewPager2FragmentStateAdapter 中,需要创建 fragment 实例。
相关分页的系列操作就放在了该 Fragment中后,场景:在每切换一次 tab,都需要刷新每个 tab 对应的网络请求;之后,再在当前 tab 中加载更多的分页数据;再切换 tab,来回往复操作… 后来发现个 bug:有些 tab 仅加载一页后,loadMore 视图不显示了

解决方法:先是重置了adapter 的数据源绑定,后来发现,还是会有问题。又重置了所有 adapter 的实例,解决了…

private var isResetAdapter = false // 是否重置

private val helper by lazy {
    object : PageUiHelper<ItemData>(adapter) {
        override fun onNotLoadingOrErrorState() {
            dismissLoadingDialog()
            if (isResetAdapter && adapter.itemCount > 0) {
                // 由于重置数据源后,若有新增数据,会排在最前面,rv 不会自动滚动,需要代码操作滚动
                mBind.rvAll.scrollToPosition(0)
                isResetAdapter = false
            }
        }
    }
}

fun loadData() {
    viewLifecycleOwner.lifecycleScope.launchWhenResumed {
        helper.init()
        mBind.rvAll.adapter = helper.getMainAdapter()
        
		showLoadingDialog()
		
        isResetAdapter = true
        mViewModel.getAllData().collectLatest { // 加载数据
            adapter.submitData(it) // 绑定分页数据源
        }
    }
}

刷新 adapter 后,发现展现的数据内容没有更新?(更新于 230904)

很经典的场景:
从 adapter 点击item,打开数据编辑页,修改后,回到列表页,展示的内容数据不刷新;实际上,已经重置了 adapter 的分页数据源。

PagingDataAdapter,有个判断内容是否相同的函数 areContentsTheSame()。如果返回 true,表示是相同的内容,不刷新 item。
比如,除了 id 标识,页面上要展示名字(name)、电话号码(phone)文章来源地址https://www.toymoban.com/news/detail-626891.html

// 是否内容相同
override fun areContentsTheSame(oldItem: CustomerData, newItem: CustomerData): Boolean {
    logi("areContentsTheSame")
    return oldItem.id == newItem.id && oldItem.name == newItem.name && oldItem.phone == newItem.phone
}

到了这里,关于Android Paging3分页+ConcatAdapter+空数据视图+下拉刷新(SwipeRefreshLayout)+加载更多+错误重试 (示例)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android -- 下拉列表、列表视图、网格视图

    Spinner 用于从一串列表中选择某项,功能类似于单选按钮的组合 下拉列表的展示 方式有两种,一种是在当前下拉框的正下方弹出列表框,此时要把spinnerMode属性设置为 dropdown,另一种是在页面中部弹出列表对话框,此时要把spinnerMode属性设置为dialog。 dropdown 下拉模式       

    2023年04月08日
    浏览(32)
  • 【OSTEP】分页(Paging) | 页表中究竟有什么 | 页表存在哪 | 内存追踪

      💭 写在前面 本系列博客为复习操作系统导论的笔记,内容主要参考自: Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau, Operating Systems: Three Easy PiecesA. Silberschatz, P. Galvin, and G. Gagne, Operating System Concepts, 9th Edition, John Wiley Sons, Inc., 2014, ISBN 978-1-118-09375-7.Microsoft. MSDN(Microsoft Developer

    2024年02月04日
    浏览(39)
  • 前后端分离项目(六):数据分页查询(前端视图)

    🚀 优质资源分享 🚀 🧡 Python实战微信订餐小程序 🧡 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。 💛Python量化交易实战💛 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统 好家伙,该项

    2024年02月07日
    浏览(32)
  • elementUi select下拉框触底加载异步分页数据

    在Element UI中,可以通过监听select下拉框的 visible-change 事件来实现触底加载下一页的效果。 方式一:利用elementUi的事件 具体步骤如下: 首先,在select组件中设置: @visible-change=\\\"handleVisibleChange\\\" ref=\\\"mySelect\\\" 在data中定义一个变量pageNum,用于记录当前加载的页码: pageNum: 1, 在m

    2024年02月14日
    浏览(26)
  • el-select下拉框处理分页数据,触底加载更多

    1、声明自定义指令: 2、使用自定义指令v-loadmore: 3、发送请求加载数据 参考:el-select滚动到底部加载更多(分页加载数据)_el-select 触底加载分页_天道酬勤_鹿的博客-CSDN博客

    2024年02月16日
    浏览(26)
  • 如何vue使用ant design Vue中的select组件实现下拉分页加载数据,并解决存在的一个问题。

        需求:拉下菜单中数据过多,200条以上,就会导致select组件卡死。所以需要使用滑动到底部使其分页加载     可以借助 onPopupScroll 事件来监听下拉菜单的滚动事件,并判断当前是否已经到达了下拉菜单底部。具体可以通过以下步骤实现:     1、在组件中绑定 @popupScro

    2023年04月20日
    浏览(42)
  • 【QT性能优化】QT性能优化之QT6框架高性能模型视图代理框架千万级数据表分页查询优化

    QT性能优化之QT6框架高性能模型视图代理框架千万级数据表分页查询优化 简介 本文介绍了QT模型视图代理框架中的QT表格控件和QT数据库模块中的QT数据库查询模型结合使用的一个应用实践案例:QT高性能表格控件分页展示千万行数据。本文介绍了这个应用实践案例的运行效果

    2024年02月14日
    浏览(36)
  • Padding负值遮挡视图(下拉刷新头部)

    padding值 为 负值 时,表示 当前视图被遮住了一部分 。 可使用 view .set Padding(  int left , int top , int right , int bottom ) 方法进行动态设置。 下拉刷新头部的实现: (1) 获取屏幕大小。 (2) 设置 刷新头高度(定值)      设置 显示部分高度为/屏幕高度(定值)      设置 整体高度

    2024年02月06日
    浏览(35)
  • Android中的RecyclerView下拉/上拉刷新数据

            在Android中的列表视图(我们这里以RecyclerView为例)中有很多数据的时候,往往要采取限制数据条目显示,然后通过刷新再添加新的数据显示,这样看的就会比较美观,那么这种列表视图是怎么实现刷新的呢,我们一起来看看吧。 我们先看看美团的刷新 美团下拉/上拉

    2024年02月12日
    浏览(27)
  • 前端vue简单好用的上拉加载下拉刷新组件,支持列表分页 本地分页

    前端vue简单好用的上拉加载下拉刷新组件,支持列表分页 本地分页, 阅读全文下载完整代码请关注微信公众号: 前端组件开发 效果图如下:         #### 使用方法 ```使用方法 !-- pullDown:下拉刷新 back-top: 回到顶部  -- ccPullScroll class=\\\"pullScrollView\\\" ref=\\\"pullScroll\\\" :back-top=\\\"true\\\" :pullDo

    2024年02月08日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包