JetPack 之 Pagging3
1.Pagging3是什么
Pagging3是帮助用来做加载和显示
本地存储或者处理更大的数据集(网络)的数据页面.
简单来讲,就是一个分页加载库.
具有的好处为:
1.只需要按照它提供的架构去编写业务逻辑,就可以轻松实现分页功能。
2.非监听列表滑动事件.
实现效果为:
2.Pagging3的依赖
def paging_version = "3.1.1"
//Java依赖
implementation "androidx.paging:paging-runtime:$paging_version"
//Kotlin依赖
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
Paging 库概览 | Android 开发者 | Android Developers
网络的请求可参考以下博客:
Jetpack新成员,Paging3从吐槽到真香_guolin的博客-CSDN博客_paging
3.Pagging3的用法
主要包含以下内容:
- 配置数据源:PagingSource
- 构建分页数据:Pager、PagingData
- 构建RecyclerView Adapter:PagingDataAdapter
- 展示分页UI列表数据
这里我们选用自己的任意一个List作为目标数据
class PaggingViewModel : ViewModel() {
val lazyInitStateFlow bylazy{
var list:MutableList<String> = ArrayList()
for(d in 0..60){list.add(d.toString())}
MutableStateFlow(list).also{
it.value = list
}
}
}
配置数据源Source:
这里需要对代码进行解释,继承该PaggingSource需要声明两个范类,Int→key代表当前页数,String为每一项的数据.
可以看到
nextPageNumber
为我当前的页数
response
为我每一页的数据
最后需要调用LoadResult.Page()函数,构建一个LoadResult对象并返回
data
是你需要展示的数据
prevKey
为上一页的页数
nextKey
就是下一页的页数
对于nextKey
的理解可以是,你去判断什么情况下可以有下一页,这一页的数据是什么.
//配置数据源.
// Key为分页标识 ,Value为返回列表元素类型
class ExamplePagingSource(
val viewmodel: PaggingViewModel,
) : PagingSource<Int, String>() {
val TAG = "ExamplePagingSource"
var SplitLocation = 0
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, String> {
//如果加载成功,则返回 LoadResult.Page 对象。
//如果加载失败,则返回 LoadResult.Error 对象。
try {
// Start refresh at page 1 if undefined.
val nextPageNumber = params.key ?: 1
val response = viewmodel.lazyInitStateFlow.value.subList(SplitLocation,SplitLocation+params.loadSize -1 )
val ListX = viewmodel.lazyInitStateFlow.value
SplitLocation = SplitLocation + params.loadSize
return LoadResult.Page(
data =response,
prevKey = null, // Only paging forward.if(nextPageNumber >1) nextPageNumber -1 else null
nextKey = if (response.isNotEmpty()) nextPageNumber + 1 else null
)
} catch (e: Exception) {
// Handle errors in this block and return LoadResult.Error if it is an
// expected error (such as a network failure).
return LoadResult.Error(e)
}
}
// override fun getRefreshKey(state: PagingState<Int, String>): Int? = null
override fun getRefreshKey(state: PagingState<Int, String>): Int? {
// Try to find the page key of the closest page to anchorPosition, from
// either the prevKey or the nextKey, but you need to handle nullability
// here:
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey null -> anchorPage is the initial page, so
// just return null.
return state.anchorPosition?.let{anchorPosition->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
构建分页的数据Pagger,PagingData:
基本解释都在注释内了,可以自己稍微看一下
//作为分页数据
val AllData: Flow<PagingData<String>> = Pager(
PagingConfig(
// 每页显示的数据的大小。对应 PagingSource 里 LoadParams.loadSize
pageSize = 10,
// 预刷新的距离,距离最后一个 item 多远时加载数据,默认为 pageSize
prefetchDistance = 3,
// 初始化加载数量,默认为 pageSize * 3
initialLoadSize = 20,
// 一次应在内存中保存的最大数据,默认为 Int.MAX_VALUE
maxSize = 200
),
){
// 数据源,要求返回的是 PagingSource 类型对象
ExamplePagingSource(this)
}.flow.cachedIn(viewModelScope) // 最后构造的和外部交互对象,有 flow 和 liveData 两种
构建RecyclerView Adapter→PagingDataAdapter:
这里比较特殊的是,它必须继承于PagingDataAdapter
,并且构造需要传入DiffUtil.
DiffUtil为计算刷新的工具类.
Android RecyclerView的正确打开方式–DiffUtil、AsyncListDiff以及最新ListAdapter使用_e电动小马达e的博客-CSDN博客
getItem(position)就是获取到设置的每一项中数据源是什么
// 构建RecyclerView Adapter
class PaggingAdapter : PagingDataAdapter<String, PaggingAdapter.PaggingViewHolder>(UserDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaggingViewHolder {
val binding = AdapterItemButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PaggingViewHolder(binding)
}
override fun onBindViewHolder(holder: PaggingViewHolder, position: Int) {
// Log.d("TAG", "onBindViewHolder: ${getItem(position)}")
(holder.binding as AdapterItemButtonBinding).adapterItemButton.setText(getItem(position).toString())
(holder.binding as AdapterItemButtonBinding).adapterItemButton.setOnClickListener{Log.d("TAG", "onBindViewHolder: 我是按钮RecyclView")}
}
override fun getItemCount(): Int {
return super.getItemCount()
}
class PaggingViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
init {
}
}
}
//计算位置刷新的工具类
private class UserDiffCallback : DiffUtil.ItemCallback<String>() {
// 返回两个item是否相同
// 例如:此处两个item的数据实体是User类,所以以id作为两个item是否相同的依据
// 即此处返回两个user的id是否相同
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
// 当areItemsTheSame返回true时,我们还需要判断两个item的内容是否相同
// 此处以User的age作为两个item内容是否相同的依据
// 即返回两个user的age是否相同
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
展示分页UI列表数据
然后就简简单单的去设置RecycleView的adapter就可以了
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
bind.PaggingRecycleView.layoutManager = LinearLayoutManager(requireContext())
bind.PaggingRecycleView.adapter = PaggingAdapter
viewModel.viewModelScope.launch{
viewModel.AllData.collectLatest{
PaggingAdapter.submitData(it)
}
}
}
4.Pagging3的扩展
- 监听数据加载的状态
- 设置Header和Footer,在底部显示加载状态
监听数据加载的状态:
如果想要监听数据获取的状态,可以使用该方法
addLoadStateListener
:这个方法就是当新的PagingData被提交并且显示的回调
- refresh:LoadState:刷新时的状态,因为可以调用 PagingDataAdapter#refresh() 方法进行数据刷新
- append:LoadState:可以理解为 RecyclerView 向下滑时数据的请求状态。
- prepend:LoadState:可以理解为RecyclerView 向上滑时数据的请求状态。
PaggingAdapter.addLoadStateListener{
when (it.append) {
is LoadState.NotLoading -> {
Log.d(TAG, "append: 未加载状态")}
is LoadState.Loading -> {
Log.d(TAG, "append: 正在加载")}
is LoadState.Error -> {
Log.d(TAG, "append: 加载失败")
val state =it.append as LoadState.Error
Toast.makeText(requireContext(), "Load Error: ${state.error.message}", Toast.LENGTH_SHORT).show()
}
}
设置Header和Footer,在底部显示加载状态
一般来讲,如果不是下拉再获取数据,而是提前加载好了更多的数据,是不会在底部看到加载状态的.因为Pagging会在还未滑到底部的时候去提前加载数据了.
但是网络总有不好的时候.此时应该在列表底部增加一个Loading和重试按钮
FooterAdapter作为RecyeleView的底部适配器,注意它需继承LoadStateAdapter
class FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<PaggingAdapter.PaggingViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): PaggingAdapter.PaggingViewHolder {
val binding = PaggingAdapterFootLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return PaggingAdapter.PaggingViewHolder(binding)
}
override fun onBindViewHolder(holder: PaggingAdapter.PaggingViewHolder, loadState: LoadState) {
(holder.binding as PaggingAdapterFootLayoutBinding).retryButton.setOnClickListener{retry()}
holder.binding.progressBar.isVisible= loadState is LoadState.Loading
holder.binding.retryButton.isVisible= loadState is LoadState.Error
}
}
然后设置进去就可以了.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
///....
bind.PaggingRecycleView.adapter = InternalAdapter.withLoadStateFooter(FooterAdapter { PaggingAdapter.retry() })
///....
}