Paging3的尝鲜
Paging 3 的尝鲜
前言(伪)
咕咕咕 x n,想不到一咕就这么久,有点惭愧,好歹良心发现,开始继续更新。
前言
之前分享了Paging 2 的相关使用,说实话确实不怎么好用,这不Paging 3来了,虽然现在还是alpha
版,但是日常使用基本是没问题的,目前的最新版是3.0.0-alpha09
,这次的例子是使用kotlin
进行开发的,以后也是。还有就是我写的比较啰嗦,如果嫌太多的话可以看官方的Demo
写的也是比较详细
这次使用的API
接口是WanAndroid
的首页文章列表:https://www.wanandroid.com/article/list/0/json
好了废话不多说,直接开始
食材准备
首先先导入相关的的库,网络请求用的是Retrofit
def paging_version = "3.0.0-alpha09"
implementation "androidx.paging:paging-runtime:$paging_version"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.code.gson:gson:2.8.
然后根据返回的json
定义接口和数据类,这里为了节省时间,只接收了部分数据,如果觉得json
格式在浏览器里查看比较辣眼睛的话,可以在Android Studio
中创建scratch
文件
interface WanAndroidApi {
@GET("https://www.wanandroid.com/article/list/{page}/json")
suspend fun getArticles(
@Path("page") page: Int
) : BaseResponse<ArticleInfo>
}
data class BaseResponse<T>(
@SerializedName("data")
val data: T,
@SerializedName("errorCode")
val errorCode: Int,
@SerializedName("errorMsg")
val errorMsg: String
) : Serializable
data class ArticleInfo(
@SerializedName("curPage")
val currentPage: Int,
@SerializedName("datas")
val articleList: List<Article>
) : Serializable
data class Article(
@SerializedName("id")
val id: Long,
@SerializedName("title")
val title: String,
@SerializedName("author")
val author: String
) : Serializable
顺带写个Retrofit的初始化类,方便后面使用
object RetrofitUtils {
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://www.wanandroid.com")
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())).build()
}
fun <T> create(mClass: Class<T>) : T {
return retrofit.create(mClass)
}
}
然后开始准备Paging
所需要的东西,首先需要一个PagingSource
,在Paging 3
中PageKeyedDataSource
PositionalDataSource
ItemKeyedDataSource
都归并到PagingSource
,只需要重写load
方法即可
class ArticlePagingSource(
private val articleApi: WanAndroidApi
) : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val page = params.key ?: 0
return try {
val response = articleApi.getArticles(page)
if (response.errorCode == 0) {
LoadResult.Page(
data = response.data.articleList,
prevKey = null,
nextKey = if (response.data.articleList.isEmpty()) null else page + 1
)
} else {
LoadResult.Error(Throwable(response.errorMsg))
}
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(e)
}
}
}
然后继续往上写就是Repository
去配置Page
的相关信息,包括分页数量,初始加载数量等,这里注意Flow
的包不要引错了:kotlinx.coroutines.flow.Flow
class ArticleRepository {
fun getArticles(
articleApi: WanAndroidApi
): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(pageSize = 10, initialLoadSize = 20),
pagingSourceFactory = { ArticlePagingSource(articleApi) }
).flow
}
}
默认初始化的数量是pageSize
的三倍,我们这里把他调小一点
这里顺带把ViewModel
也写了吧
class ArticleViewModel : ViewModel() {
private val repository: ArticleRepository by lazy { ArticleRepository() }
private val articleApi: WanAndroidApi = RetrofitUtils.create(WanAndroidApi::class.java)
fun getArticles() : Flow<PagingData<Article>> {
return repository.getArticles(articleApi)
}
}
然后就是适配器了,Paging3
的适配器也和之前的不一样,之前是PagedListAdapter
,而现在是PagingDataAdapter
,基本和Paging2
的写法一致
class ArticlePagingDataAdapter : PagingDataAdapter<Article, ArticlePagingDataAdapter.ViewHolder>(ArticleComparator) {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView? = itemView.title
val author: TextView? = itemView.author
}
companion object {
val ArticleComparator = object : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem
}
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
holder.title?.text = item?.title
item?.author?.let {
holder.author?.text = if (it.isEmpty()) "Unknown" else it
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_article, parent, false))
}
}
其实本来不想贴代码的,但是考虑到后面还要在这基础上改,还是贴一下把,布局文件就自行发挥把
开始烹饪
前面用到的食材都准备好了,开始起锅烧油,在Activity
中获取数据并展示
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = ArticlePagingDataAdapter()
recyclerView.adapter = adapter
lifecycleScope.launchWhenCreated {
viewModel.getArticles().collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
OK,这样就完成了,是不是很简单,看下效果图
image-20201129162901468.png调味提鲜
通过上面的步骤已经能够完成一道“菜”了,但是有些单调,需要给他加点料
我们可以对加载的状态进行监听,来根据不同状态给予不同的提示,提升用户体验,以下对加载中、加载完成以及加载失败三种状态进行监听
adapter.addLoadStateListener {
when (it.refresh) {
is LoadState.Loading -> {
loadStateHint.isVisible = true
recyclerView.isVisible = false
loadStateHint.text = "加载中..."
}
is LoadState.NotLoading -> {
if (adapter.snapshot().items.isEmpty()) {
loadStateHint.isVisible = true
recyclerView.isVisible = false
loadStateHint.text = "暂无数据"
} else {
loadStateHint.isVisible = false
recyclerView.isVisible = true
}
}
is LoadState.Error -> {
loadStateHint.isVisible = true
recyclerView.isVisible = false
loadStateHint.text = "加载失败请重试"
loadStateHint.setOnClickListener { adapter.retry() }
}
}
}
这时有人问:一般列表往下滚动加载时底部都有那种加载框的,你这个不太行啊,我啪的一下就敲出来了,很快啊,因为Paging3
提供了顶部和底部的方式
写一个FooterAdapter
,注意这里是继承LoadStateAdapter
class FooterLoadStateAdapter(private val retry: () -> Unit) :
LoadStateAdapter<FooterLoadStateAdapter.ViewHolder>() {
class ViewHolder(retry: () -> Unit, itemView: View) : RecyclerView.ViewHolder(itemView) {
val loadStateHint: TextView? = itemView.loadStateHint
val progressBar: ProgressBar? = itemView.progressBar
init {
loadStateHint?.setOnClickListener { retry.invoke() }
}
}
override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
holder.progressBar?.isVisible = loadState is LoadState.Loading
when (loadState) {
is LoadState.Error -> {
holder.loadStateHint?.text = "加载失败,点击重试"
}
is LoadState.Loading -> {
holder.loadStateHint?.text = "加载中..."
}
else -> {
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
return ViewHolder(
retry,
LayoutInflater.from(parent.context)
.inflate(R.layout.layout_footer_load_state, parent, false)
)
}
}
然后在Activity
中设置一下就可以了
recyclerView.adapter = adapter.withLoadStateFooter(FooterLoadStateAdapter {adapter.retry()})
看下效果:
image-20201129171735074.png这里要注意一点,就是这个只会在Loading
或者是Error
状态下才会出现的,我一开始还想用于列表的Footer
,是我大意了啊
到这已经是个合格的列表了
结尾
为了实现列表的分隔符,我们需要把数据对象和分割对象装到一起,现在对ViewModel
做相关调整
class ArticleViewModel : ViewModel() {
private val repository: ArticleRepository by lazy { ArticleRepository() }
private val articleApi: WanAndroidApi = RetrofitUtils.create(WanAndroidApi::class.java)
fun getArticles() : Flow<PagingData<UiModel>> {
return repository.getArticles(articleApi)
.map { pagingData -> pagingData.map { UiModel.ArticleItem(it) } }
.map {
it.insertSeparators<UiModel.ArticleItem, UiModel> { before, after ->
if (before == null) {
return@insertSeparators null
}
if (after == null) {
return@insertSeparators null
}
return@insertSeparators UiModel.SeparatorItem(after.article.id)
}
}
}
sealed class UiModel {
data class ArticleItem(val article: Article) : UiModel()
// 注意这里不一定要填id,只是需要一个唯一标识
data class SeparatorItem(val articleId: Long) : UiModel()
}
}
我们使用了密封类来封装数据对象和分割对象,接下去需要修改适配器,以匹配修改后的返回对象,如果有写过RecyclerView
的多布局,那么以下代码肯定也是很容易看懂,要是没写过,那还愣着干嘛,补课去
class ArticlePagingDataAdapter :
PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(
ArticleComparator
) {
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val title: TextView? = itemView.title
val author: TextView? = itemView.author
}
class SeparatorViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
companion object {
val ArticleComparator = object : DiffUtil.ItemCallback<UiModel>() {
override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
return (oldItem is UiModel.ArticleItem && newItem is UiModel.ArticleItem &&
oldItem.article.id == newItem.article.id) || (oldItem is UiModel.SeparatorItem &&
newItem is UiModel.SeparatorItem && oldItem.articleId == newItem.articleId)
}
override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
return oldItem == newItem
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { uiModel ->
when (uiModel) {
is UiModel.ArticleItem -> {
holder as ViewHolder
holder.title?.text = uiModel.article.title
uiModel.article.author.let {
holder.author?.text = if (it.isEmpty()) "Unknown" else it
}
}
is UiModel.SeparatorItem -> {
}
}
}
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is UiModel.ArticleItem -> {
R.layout.item_article
}
is UiModel.SeparatorItem -> {
R.layout.item_separator
}
else -> {
throw UnsupportedOperationException("Unknown View")
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == R.layout.item_article) {
ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_article,
parent,
false
)
)
} else {
SeparatorViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_separator,
parent,
false
)
)
}
}
}
基本上就改了两个部分:一个是把Article
替换成UiModel
,毕竟数据对象变了呀;还有就是多视图的判断
运行结果如下:
image-20201129205824713.png到这关于Paging3
的使用就差不多结束了,基本能够满足日常使用需求了,当然在使用过程中也遇到些问题,不知道是Bug
还是我写的有问题:
- 在折叠状态栏下,即列表加载时最后一项会显示不全
- 在
BottomSheetDialogFragment
中,如果我删除某一项,并刷新数据,列表数据刷新成功,但是UI
部分删除的一项会有空白占位符,就像这样
如果各位有什么想法或者建议欢迎留言讨论~