Jetpack 之 Paging 3 小白入手

声明 : https://www.jianshu.com/p/714062a9af75
目录

简介
原理
使用方法

1, PagingSource 简单使用
2,添加尾布局,实现点击加载更多

🌺 简介:

  Paging 是Google 为了开发人员更方便的完成分页加载而设计的一个组件.
  这个库需要配合 kotlin 协程使用哦~
  

🌺 原理:

image.png

  🍊1,在 RecycleView 的滑动过程中,会触发 PagingDataAdapter 类中的 onBindViewHodler()方法.数据与 RecycleView Item 布局中 UI 控件正是在这个方法中绑定的.
  🍊2,在 RecycleView滑动到底部的时候,在 onBindViewHolder 的方法中所调用的 getItem() 方法会通知 PageList,当前需要载入更多数据.
  🍊3,接着,PagedList 会根据 PagedList.Config 中的配置通知 PagingSource 执行具体的数据获取工作.
  🍊4, PagingSource 从网络/本地数据库取得数据后,交给 PagedList,PagedList将持有这些数据.
  🍊5,PagedList 将数据交给 PagedListAdapter 中的 DiffUtil 进行的比对和处理.
  🍊6,数据在经过处理后,交给 RecycleView进行展示.

🌺 使用方法:

ps:本文内容涉及到了 kotlin 的协程功能.

🥝 1, build.gradle
    implementation "androidx.paging:paging-runtime:3.0.0"
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

  这里我引用了 Retrofit 库,因为我要进行网络请求数据,通过Paging 3进行分页展示.
  使用的 api 是wanandroid 开放的 api.

https://wanandroid.com/article/listproject/{pageNum}/json

方法:GET
参数:页码,拼接在连接中,从0开始。
🥝 2,创建 model
data class WnaAndroidBean(
      val data: Data,
      val errorCode: Int,
      val errorMsg: String
)

data class Data(
      val curPage: Int,
      val datas: List<SubData>,
      val offset: Int,
      val over: Boolean,
      val pageCount: Int,
      val size: Int,
      val total: Int
)

data class SubData(
      val desc: String,
      val title: String,
      val author: String,
)

  我这是精简版的,源数据很多,但是用不到,就挑了几个使.

🥝 3,创建ApiService
interface ApiService {

    @GET("article/listproject/{pageNum}/json")
    suspend fun searchProject(@Path("pageNum") pageNum: Int): WnaAndroidBean
    
    companion object {
         var apiService: ApiService = Retrofit.Builder()
            .baseUrl("https://wanandroid.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
    
}

  suspend 修饰符和挂起函数,不可缺少哦

🥝 4,定义数据源,创建WanAndroidDataSource
class WanAndroidDataSource(private val apiService: ApiService) : PagingSource<Int, SubData>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SubData> {
        return try {
            var pageNum = params.key ?: 1  //获取当前的页数,若为空,默认值为 1
            // var loadSize = params.loadSize //每一页包含多少条数据,可以设置固定,也可以灵活设置,我们这个项目,暂时用不到
            var searchProject = apiService.searchProject(pageNum)   //请求数据
            var datas = searchProject.data.datas
            val prevKey = if (pageNum > 1) pageNum - 1 else null  ///上一页
            val nextKey = if (!searchProject.data.over) pageNum + 1 else null  //下一页
            LoadResult.Page(datas, prevKey, nextKey)
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    //实现必须定义如何从已加载分页数据的中间恢复刷新,使用 state.anchorPosition 作为最近访问的索引来映射正确的初始键。
    //高级用法,和本文无关
    override fun getRefreshKey(state: PagingState<Int, SubData>): Int? = null
}

  🍑 PagingSource 对象可以从任何单个数据源(包括网络来源和本地数据库)加载数据.
  🍑 该类继承 PagingSource 需要声明两个泛型<Key, Value> 。key 定义了用于加载数据的标识符,value 是数据本身的类型。例如,如果您通过将 Int 页码传递给 Retrofit 来从网络加载各页 SubData 对象,则应选择 Int 作为 Key 类型,选择 SubData 作为 Value 类型。
  🍑 LoadParams 对象包含有关要执行的加载操作的信息,其中包括要加载的键和要加载的项数
  🍑 load () 是一个挂起函数,处理数据请求, params.key 代表当前页, params.loadSize 代表加载的数据量,最后调用 LoadResult.Page() 函数返回 LoadResult 对象.三个参数,分别是数据列表,上一个,下一页
  🍑 getRefreshKey() 函数,当数据在初始加载后刷新或失效时,该方法会返回要传递给 load() 方法的键。在后续刷新数据时,Paging 库会自动调用此方法。本文暂时用不到.

🥝 5,创建 WanAndroidViewModel 设置 PagingData.
class WanAndroidViewModel : ViewModel() {

    fun getPageData(): Flow<PagingData<SubData>> {
        return Pager(
            config = PagingConfig(10),
            pagingSourceFactory = { WanAndroidDataSource(ApiService.apiService) }).flow.cachedIn(
            viewModelScope
        )
    }

}

  🍒 PagingData 对象是用于存放分页数据的容器,它会查询 PagingSource 对象并存储结果。
  🍒 Pager 类提供的方法可显示来自 PagingSource 的 PagingData 对象的响应式流.
  🍒 这里用到了 kotlin 的协程技术,写法是固定的,实现Flow<PagingData<XXXXX>>,接口XXXXX 是灵活的,替换成列表展示的实例.返回Flow 对象.
  🍒 创建Pager对象的时候,需要传递pageSize,也就是我们每页加载多少数据和pagingSourceFactory,也就是我们之前创建好的 WanAndroidDataSource.
  🍒 Flow 提供了一个方法 cachedIn() 使数据流可共享,并使用提供的 CoroutineScope 缓存加载的数据。此示例使用 Lifecycle lifecycle-viewmodel-ktx 工件提供的 viewModelScope.这样一来,当手机屏幕发生旋转时,会从缓存中读取数据,不会发起网络请求.

🥝 6,创建PagingAdapter
class PagingAdapter : PagingDataAdapter<SubData, PagingAdapter.MyViewHolder>(DATA_COMPARATOR) {
    
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        var item = getItem(position)
        if (item != null) {
            holder.author.text = item.author
            holder.tile.text = item.title
            holder.desc.text = item.desc
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        return MyViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_paging, parent, false)
        )
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val author: TextView = itemView.findViewById(R.id.textView7)
        val tile: TextView = itemView.findViewById(R.id.textView8)
        val desc: TextView = itemView.findViewById(R.id.textView9)
    }

 
}

object DATA_COMPARATOR : DiffUtil.ItemCallback<SubData>() {
    //检查两个对象是否代表同一个项目
    override fun areItemsTheSame(oldItem: SubData, newItem: SubData): Boolean {
        return oldItem.title == newItem.title
    }

    //检查两个项目是否具有相同的数据
    override fun areContentsTheSame(oldItem: SubData, newItem: SubData): Boolean {
        return oldItem == newItem
    }
}
}

  🍓 该类继承 PagingDataAdapter ,实现 onCreateViewHolder()onBindViewHolder() 方法,并指定 DiffUtil.ItemCallback
  🍓 DiffUtil.ItemCallback 用于计算列表中两个非空项目之间差异的回调,之前我们刷新,都用notifyDataSetChanged()全部刷新一遍,现在用 DiffUtil 比对新、旧两个数据集的差异,生成旧数据到新数据的最小变动,然后对有变动的数据项,进行局部刷新。
  🍓 areItemsTheSame() 判断对象是否一致,如果不一样,则直接更新数据,如果一样,areContentsTheSame() 则判断对象内容是否一样.如果一样,不更新做额外的操作,如果不一样,则更新数据.
  🐮🐮🐮 强烈推荐 DiffUtil.ItemCallback学习连接: https://segmentfault.com/a/1190000010722635

🥝 7,编写布局文件 activity_paging.xml 自己 item 的布局文件 item_paging.xml
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.mcy.test.PagingActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

  只有一个 RecycleView,很简单

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/textView7"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="author" />

    <TextView
        android:id="@+id/textView8"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="title" />

    <TextView
        android:id="@+id/textView9"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="desc" />
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#ff0000"/>

</LinearLayout>

   创建三个 TextView,分别展示 author ,title ,desc 三个字段.

🥝 8,创建PagingActivity
class PagingActivity : AppCompatActivity() {
    var pagingAdapter = PagingAdapter()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)
        recycler_view.layoutManager = LinearLayoutManager(this)
        recycler_view.adapter = pagingAdapter
        lifecycleScope.launchWhenCreated {
            WanAndroidViewModel().getPageData().collectLatest {
                pagingAdapter.submitData(it)
            }
        }
        pagingAdapter.addLoadStateListener {
            when (it.refresh) {
                is LoadState.NotLoading -> {
                    Log.e("mmm","没有加载,加载数据前和加载数据完成后回调")
                }
                is LoadState.Loading -> {
                    Log.e("mmm", "正在加载")
                }
                is LoadState.Error -> {
                    val state = it.refresh as LoadState.Error
                    Log.e("mmm", "加载错误$state")
                }
            }
        }
    }
}

  🍇 pagingAdapter.submitData(it)是一个协程挂起(suspend)操作,所以要放入协程赋值.所以要放在 lifecycleScope.launch()函数来启动一个协程
  🍇 submitData()要接受 PagingData 类型的参数,,调用WanAndroidViewModel().getPageData().方法返回Flow 对象的collectLatest()函数才能获取到.
  🍇 collectLatest() 是末端操作符,收集 Flow 在 ViewModel 层发射出来的数据,在一段时间内发送多次数据,只会接受最新的一次发射过来的数据.
  🍇 pagingAdapter.addLoadStateListener 添加状态监听.

好了到此为止,可以运行程序啦啦 ✿✿ヽ(°▽°)ノ✿~~~~

运行截图:


E2F047B5C0A7A8670B6E05EC6AA7663D.gif
添加尾布局,比如我们加载失败或者网络不好,我们希望有个提示~~~~
🥝 1,编写item_foot.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <Button
        android:id="@+id/goon"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="继续加载" />

</LinearLayout>

  布局很简单,就一个按钮.

🥝 2,创建FooterAdapter
class FooterAdapter(private val retryLoad: () -> Unit) :
    LoadStateAdapter<FooterAdapter.MyViewHolder>() {
    
    override fun onBindViewHolder(holder: MyViewHolder, loadState: LoadState) {
        holder.goon.isVisible = loadState is LoadState.Error
        holder.goon.setOnClickListener {
            retryLoad()
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): MyViewHolder {
        return MyViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.item_foot, parent, false)
        )
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val goon: Button = itemView.findViewById(R.id.goon)
    }

}

  🍉 该类继承 LoadStateAdapter
  🍉 我们用 kotlin 高级函数给按钮添加点击事件,当按钮点击是,会执行传入的函数.

🥝 3, 修改PagingActivity
class PagingActivity : AppCompatActivity() {
    var pagingAdapter = PagingAdapter()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging)
        recycler_view.layoutManager = LinearLayoutManager(this)
        //最重要的是下面这一句
        recycler_view.adapter = pagingAdapter.withLoadStateFooter(FooterAdapter { pagingAdapter.retry() })
         //.....省略部分代码
    }
}

  🍋 调用pagingAdapter.withLoadStateFooter()函数把 FooterAdapter 传进去.
  🍋 { pagingAdapter.retry() }是Lambda 表达方式传入FooterAdapter的参数.

运行截图:


26C7F077AD1279B602A529B5A7656B56.gif

END

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

推荐阅读更多精彩内容