一、概念
相对于传统的下拉刷新上拉加载,只需要告诉Paging如何加载数据,不用再监听滑动事件操作何时加载下一页。
二、基本使用
2.1 添加依赖
查看官方最新版本
implementation "androidx.paging:paging-runtime:3.1.1"
2.2 定义数据源 PagingSource
自定义一个类继承 PagingSource 并重写 load() 来提供获取页面数据。
/**
* 参数一:页码的类型。参数二: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会调用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