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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容