一、概念
相对于传统的下拉刷新上拉加载,只需要告诉Paging如何加载数据,不用再监听滑动事件操作何时加载下一页。
PagingSource | 用于定义数据的来源和加载方式。开发者需要实现 PagingSource 抽象类,并在其中指定如何从数据源中加载特定页的数据。PagingSource 通常用于与网络 API 或本地数据库进行交互,获取分页数据。 |
Pager | 用于创建分页数据流的类。通过 Pager,开发者可以将 PagingSource 与其他配置参数(如分页大小、预取距离等)结合起来,创建用于加载和展示分页数据的 PagingData 数据流。Pager 提供了多个静态方法,用于创建不同类型的 PagingData 数据流。Pager对象负责从PagingSource加载数据,并将数据包装成PagingData对象,这个PagingData对象可以用来观察数据的变化。 |
PagingData | 表示分页数据的类。它是一个泛型类,可以容纳各种类型的分页数据。 |
PagingDataAdapter | 是 RecyclerView.Adapter 的子类,专门用于展示 PagingData 数据流中的分页数据。PagingDataAdapter 提供了内置的数据差异计算和局部刷新机制,使得在 RecyclerView 中展示分页数据变得更加高效和简单。它还提供了加载状态和错误处理等功能。 |
LoadStateAdapter | 是一个用于展示加载状态的 RecyclerView.Adapter 的子类。它可以与 PagingDataAdapter 结合使用,用于展示分页数据的加载状态,如加载中、加载错误等。LoadStateAdapter 可以显示自定义的加载状态布局,并根据加载状态的变化自动更新 UI。 |
RemoteMediator | 是用于处理远程数据加载和数据库插入的接口。当 PagingSource 加载远程数据时,RemoteMediator 可以在加载完成后将数据插入本地数据库,并提供信息以支持分页和数据持久化。RemoteMediator 是实现离线缓存和数据持久化的关键组件之一。 |
1.1 Repository 层定义数据源 PagingSource
PagingSource<Key, Value> | 第一个参数 Key:表示页的标识符。在 Paging 3 中,每个页都需要一个唯一的标识符来识别它。通常情况下,这个标识符可以是整数类型,表示页的编号或索引。在加载数据时,我们可以根据这个标识符来确定要加载的是哪一页的数据。 第二个参数 Value:表示加载的数据项的类型。这个类型可以是你自定义的任何数据类型,根据你的需求而定。在分页加载过程中,每个加载的数据项都属于这个类型。 | |
getRefreshKey() | 请求出错时会调用refresh方法加载 ,如果当前已经请求了第一页到第四页的数据, 可以通过设置在refresh 后会加载第5 - 8页的数据,并且前四页的数据都没了。如果getRefreshKey返回null,refresh后 会重新加载第一到第四页的数据。 | |
load() | 负责加载特定页的数据。(编写具体的数据加载逻辑、处理数据加载状态和错误、设置下一页或者前一页的key值)。 | 参数 LoadParams 包含了当前请求的加载信息,例如 params.key(当前请求的页数)、params.loadSize(请求的加载数量)等。 |
返回值 LoadResult 是一个包装类,用于封装加载结果,它可以是: LoadResult.Page:表示成功加载了一页数据,需要提供:data(当前页的数据)、prevKey(前一页)、nextKey(后一页)。 LoadResult.Error:表示加载数据时遇到了错误,需要提供一个Throwable对象。 |
1.2 ViewModel 层定义 Pager
Pager 负责将 PagingSource 与界面进行绑定,并提供可供界面使用的数据类型。
Pager | Pager ( config: PagingConfig, //加载配置 initialKey: Key? = null, //可选的初始化数据,指定初始页的键值 remoteMediator: RemoteMediator<Key, Value>?, pagingSourceFactory: () -> PagingSource<Key, Value> //指定数据源 ) |
PagingConfig | pageSize:指定每页加载多少项数据。 prefetchDistance:预取下一页数据的距离,不能为0,否则不会拉取下一页数据。 initialLoadSize:初始加载多少项数据,跟pageSize保持一致就好 |
二、基本使用
2.1 添加依赖
implementation "androidx.paging:paging-runtime:3.1.1"
2.2 定义数据源 PagingSource
/**
* 参数一:页码的类型。参数二:item的类型(注意不是表)。
* PagingSource会在ViewModel中调用,ViewModel一般写法会创建Repository实例,
* 由于Repository中对于数据来源进行了封装,因此构造传入Repository实例来调用获取数据的方法会更好。
*/
class MyPagingSource(
private val repository: MyRepository
) : PagingSource<Int, HotNewestArticleBean.HotNewestArticle.Article>() { //参数一页码,参数二API返回数据对应的实体类
private val startPage = 0 //API的默认开始页码
//提供对应页面的数据(分页逻辑)
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, HotNewestArticleBean.HotNewestArticle.Article> {
return try {
//拿到当前页码(为null就设置为默认开始页码)
val currentPage = params.key ?: startPage
//当前页获取数据逻辑(数据源在内部自己管理,而不是在Adapter中了)
val data = getData(currentPage)
//上一页页码(当前页是第一页,上一页就返回null)
val prevKey = if (currentPage > startPage) currentPage - 1 else null
//下一页页码(当前页是最后一页,下一页就返回null)
val nextKey = if (data.isNotEmpty()) currentPage + 1 else null
//返回结果
LoadResult.Page(data, prevKey, nextKey)
} catch (e: Exception) {
//返回错误
LoadResult.Error(e)
}
}
//控制刷新时,从最后请求页面加载,还是设为null从第一页加载。
//例如当前已经请求了1-4页,可以通过设置在刷新后会加载5-8页数据,而1-4页数据都没了。
override fun getRefreshKey(state: PagingState<Int, HotNewestArticleBean.HotNewestArticle.Article>): Int? {
return null
}
//具体获取数据的方法。这里能更细分的对异常处理,否则在load()中合并返回后在UI中难区分。
//但处理后还是要抛出异常,不然load()不会返回异常,影响UI中对Paging状态判断
private suspend fun getData(currentPage: Int): List<HotNewestArticleBean.HotNewestArticle.Article> {
var data: List<HotNewestArticleBean.HotNewestArticle.Article> = emptyList()
runCatching {
repository.getData(currentPage.toString()) //通过仓库获取数据,
}.onSuccess { response ->
response.getData().onSuccess {
data = it.datas
}.onFailure {
Log.e("服务器错误", it.message.toString())
throw Exception("服务器错误:${it.message}")
}
}.onFailure {
Log.e("本地错误", it.message.toString())
throw Exception("本地错误:${it.message}")
}
return data
}
}
2.3 ViewModel层定义Pager
Pager会调用PagingSource的load( )方法获取数据,每个PagingData代表一页的数据。
class MyViewModel : ViewModel() {
private val repository = Drawer3Repository()
val dataLiveData by lazy {
Pager(PagingConfig(pageSize = 15)) {
MyPagingSource(repository)
}.liveData.cachedIn(viewModelScope)
}
//返回值Flow<PagingData<HotNewestArticleBean.HotNewestArticle.Article>>
//PagingData<>里面包含的是item类型,这里用的是Flow,也可以用上面的LiveData
//通过PagingConfig配置一页加载的item数量(pageSize)
fun getData() = Pager(PagingConfig(pageSize = 15)) {
MyPagingSource(repository)
}.flow.cachedIn(viewModelScope) //将数据在ViewModel中缓存,横竖屏切换后Paging能从缓存中读取数据而不是重新联网请求。
}
2.4 PagingDataAdapter
//比较器DIFFCALLBACK通过伴生对象DiffUtil实现
//不需要传数据源进来,不需要实现条目数量,这些在PagingSource中进行
class MyPagingAdapter : PagingDataAdapter<HotNewestArticleBean.HotNewestArticle.Article, RecyclerView.ViewHolder>(DIFFCALLBACK) {
lateinit var binding: Drawer3ItemBinding
companion object {
private val DIFFCALLBACK = object : DiffUtil.ItemCallback<HotNewestArticleBean.HotNewestArticle.Article>() {
override fun areItemsTheSame(oldItem: HotNewestArticleBean.HotNewestArticle.Article, newItem: HotNewestArticleBean.HotNewestArticle.Article): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: HotNewestArticleBean.HotNewestArticle.Article, newItem: HotNewestArticleBean.HotNewestArticle.Article): Boolean {
return oldItem == newItem
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val myViewHolder = holder as MyViewHolder
val article = getItem(position) //拿到bean
if (article != null) {
val title = Html.fromHtml(article.title).toString()
myViewHolder.tvTitle.text = title
myViewHolder.tvTime.text = article.niceDate
val author = article.author
val shareUser = article.shareUser
val superChapterName = article.superChapterName
if (author.isEmpty()) {
myViewHolder.tvUserName.text = String.format("%s · %s", superChapterName, shareUser)
} else {
myViewHolder.tvUserName.text = String.format("%s · %s", superChapterName, author)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
binding = ItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder()
}
inner class MyViewHolder : RecyclerView.ViewHolder(binding.root) {
var tvTitle = binding.tvTitle
var tvUserName = binding.tvUsername
var tvTime = binding.tvTime
}
}
2.5 UI
private fun initView() {
val progressBar = binding.progressBar
val recyclerView = binding.recyclerView
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = MyPagingAdapter()
recyclerView.adapter = adapter
//设置加载状态监听(也可以写成adapter.loadStateFlow.collectLatest{}对it进行分类)
adapter.addLoadStateListener {
//it.refresh:在初始化刷新的使用(也就是说第二页第三页...是监听不到的)
//it.append:在加载更多的时候使用
//it.prepend:在当前列表头部添加数据的时候使用
when (it.refresh) {
//当没有加载动作并且没有错误的时候
is LoadState.NotLoading -> {
progressBar.visibility = View.INVISIBLE
recyclerView.visibility = View.VISIBLE
}
//正在加载
is LoadState.Loading -> {
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.INVISIBLE
}
//加载错误(这里的错误是PagingSource里捕获的)
is LoadState.Error -> {
val state = it.refresh as LoadState.Error
progressBar.visibility = View.INVISIBLE
showToast("adapter报错: ${state.error.message}")
}
}
}
}
private fun byFlow() {
lifecycleScope.launch {
viewModel.getData().collect { pagingData ->
adapter.submitData(pagingData) //提交数据后Paging就开始工作了
}
}
}
// private fun byLiveData() {
// viewModel.dataLiveData.observe(this) { pagingData ->
// lifecycleScope.launch {
// adapter.submitData(pagingData)
// }
// }
// }
三、添加Footer、Header
//通过构造传入重试的方法,在UI中直接传入PagingAdapter.retry()
class Drawer3FooterAdapter(val retry: () -> Unit) : LoadStateAdapter<RecyclerView.ViewHolder>() {
private lateinit var binding: Drawer3FooterBinding
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, loadState: LoadState) {
val footViewHolder = holder as FootViewHolder
//根据LoadState状态来控制脚部界面显示(加载/重试)
footViewHolder.progressBar.isVisible = loadState is LoadState.Loading
footViewHolder.retryButton.isVisible = loadState is LoadState.Error
footViewHolder.retryButton.setOnClickListener {
retry
}
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): RecyclerView.ViewHolder {
binding = Drawer3FooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return FootViewHolder()
}
inner class FootViewHolder : RecyclerView.ViewHolder(binding.root) {
val progressBar = binding.progressBar
val retryButton = binding.retryButton
}
}
adapter = MyPagingAdapter()
val concatAdapter = adapter.withLoadStateFooter(Drawer3FooterAdapter { adapter.retry() }) //添加脚部
recyclerView.adapter = concatAdapter