阅读 110

Paging3的尝鲜

Paging 3 的尝鲜

前言(伪)

咕咕咕 x n,想不到一咕就这么久,有点惭愧,好歹良心发现,开始继续更新。

前言

之前分享了Paging 2 的相关使用,说实话确实不怎么好用,这不Paging 3来了,虽然现在还是alpha版,但是日常使用基本是没问题的,目前的最新版是3.0.0-alpha09,这次的例子是使用kotlin进行开发的,以后也是。还有就是我写的比较啰嗦,如果嫌太多的话可以看官方的Demo

Android Paging codelab

写的也是比较详细

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

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

Snipaste_2020-12-20_23-29-55.png

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

Paging3Demo

作者:禄遥知马力

原文链接:https://www.jianshu.com/p/cf15454f6ad9

文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐