我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

文章目录

请添加图片描述

引入库

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.Colorimport android.view.LayoutInflaterimport android.view.ViewGroupimport android.widget.TextViewimport androidx.paging.PagingDataAdapterimport androidx.recyclerview.widget.DiffUtilimport com.stone.stoneviewskt.Rimport com.stone.stoneviewskt.common.BaseViewHolderimport com.stone.stoneviewskt.data.CustomerDataimport com.stone.stoneviewskt.util.logiclass PageListAdapter: PagingDataAdapter<CustomerData, BaseViewHolder>(object : DiffUtil.ItemCallback<CustomerData>() {    // 是否是同一条 item    override fun areItemsTheSame(oldItem: CustomerData, newItem: CustomerData): Boolean {        logi("areItemsTheSame") // 没有输出该日志  可能是官方的bug(不然实现这个回调有什么意义)        return oldItem.id == newItem.id    }    // 是否内容相同    override fun areContentsTheSame(oldItem: CustomerData, newItem: CustomerData): Boolean {        logi("areContentsTheSame") // 没有输出该日志 可能是官方的bug(不然实现这个回调有什么意义)        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

布局文件:

<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.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.widget.ProgressBarimport android.widget.TextViewimport androidx.paging.LoadStateimport androidx.paging.LoadStateAdapterimport com.stone.stoneviewskt.Rimport com.stone.stoneviewskt.common.BaseViewHolderimport com.stone.stoneviewskt.util.logiclass 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.Rimport com.stone.stoneviewskt.adapter.BaseRvAdapterimport com.stone.stoneviewskt.common.BaseViewHolderclass EmptyAdapter: BaseRvAdapter<String>(R.layout.layout_empty) {    override fun fillData(holder: BaseViewHolder, position: Int, data: String) {    }}

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


分页数据源

import androidx.paging.PagingSourceimport androidx.paging.PagingStateimport com.stone.stoneviewskt.BuildConfigimport com.stone.stoneviewskt.data.CustomerDataimport com.stone.stoneviewskt.data.CustomerPageDataimport com.stone.stoneviewskt.util.logiimport com.stone.stoneviewskt.util.showShortimport kotlin.random.Randomclass 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 提供加载数据源的方法

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> 而不是 Flow ,所以然并卵…

结合以上实现的 Fragment

import android.os.Bundleimport androidx.lifecycle.ViewModelProviderimport androidx.lifecycle.lifecycleScopeimport androidx.paging.LoadStateimport androidx.recyclerview.widget.ConcatAdapterimport com.stone.stoneviewskt.Rimport com.stone.stoneviewskt.common.BaseBindFragmentimport com.stone.stoneviewskt.databinding.FragmentPageListBindingimport com.stone.stoneviewskt.util.logiimport com.stone.stoneviewskt.util.showShortimport kotlinx.coroutines.flow.collectLatestclass 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(open val totalCount: Int, val data: List)

base 分页数据源

import androidx.paging.PagingSourceimport androidx.paging.PagingStateimport com.stone.stoneviewskt.BuildConfigimport com.stone.stoneviewskt.data.BasePageDataimport com.stone.stoneviewskt.util.logiimport com.stone.stoneviewskt.util.showShortabstract 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.BasePageDataimport com.stone.stoneviewskt.data.CustomerDataimport com.stone.stoneviewskt.util.logiclass 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.LoadStateimport androidx.paging.PagingDataAdapterimport androidx.recyclerview.widget.ConcatAdapterimport com.stone.stoneviewskt.common.BaseViewHolderimport com.stone.stoneviewskt.util.logiabstract 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) // 绑定分页数据源        }    }}

来源地址:https://blog.csdn.net/jjwwmlp456/article/details/131142710

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录