0
点赞
收藏
分享

微信扫一扫

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 3PageKeyedDataSource 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,这样就完成了,是不是很简单,看下效果图

调味提鲜

通过上面的步骤已经能够完成一道“菜”了,但是有些单调,需要给他加点料

我们可以对加载的状态进行监听,来根据不同状态给予不同的提示,提升用户体验,以下对加载中、加载完成以及加载失败三种状态进行监听

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()})

看下效果:

这里要注意一点,就是这个只会在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,毕竟数据对象变了呀;还有就是多视图的判断

运行结果如下:

到这关于Paging3的使用就差不多结束了,基本能够满足日常使用需求了,但是也碰到个问题:
比如列表设置了addItemDecoration,并且对最后一项设置不同的高度,那么在删除的时候会出现这样的情况

如果各位有什么想法或者建议欢迎留言讨论~

举报

相关推荐

0 条评论