文章目录
- 引入库
- 数据模型定义
- 分页 adapter
- 加载更多 adapter
- 空数据 adapter
- 分页数据源
- ViewModel 提供加载数据源的方法
- 结合以上实现的 Fragment
- 数据重复问题
引入库
implementation 'androidx.paging:paging-runtime-ktx:3.1.1'
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>() {
/*
* 如果 areItemsTheSame 返回 true,但是仍然会展示相同的数据,可能是因为 areContentsTheSame 返回 false
*/
// 是否是同一条 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
}
}
加载更多 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
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) {
}
}
分页数据源
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 kotlinx.coroutines.delay
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> {
if (BuildConfig.DEBUG) { // 防止加载数据过快,loading 效果看不到
delay(1000)
}
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()
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( //PagingConfig 定义一次从PagingSource加载的项数。
config = PagingConfig(
pageSize = 10,
initialLoadSize = 15, // 初始取的 pageSize,通常要大于指定的 pageSize 但大于了,在retry时又会出现bug
prefetchDistance = 1 // prefetchDistance条数据 时加载下一页
),
//每页都会调用一次,请求数据
pagingSourceFactory = { PageListPageSource() }
).flow.distinctUntilChangedBy { it }.cachedIn(viewModelScope) // 设置缓存
}
}
结合以上实现的 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)
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) // 绑定分页数据源
}
}
}
}
数据重复问题
截止到 3.1.1版本,官方库有bug:超出 initialLoadSize 所在页码后, 再retry 没问题;反之retry,会数据重复。比如 pageSize=10,initialLoadSize=15,初始加载15个数据后,当前页码对应 pageSize=10 的情形下是 2;这时再请求第2页,发生了异常,调用 adapter#retry()后,会把第二页的10个条目加载进来并显示,这就发生了数据重复。而DiffUtil.ItemCallback中的两个函数,一直都没有被回调,没有任何日志打印出来… 这就很迷幻了…
为了规避该问题,建议 真实开发时,initialLoadSize 和 pageSize 的值相同。 或看看后续版本是否有修复