Jetpack Paging3

Paging3 是 Google 官方为开发者提供的分页组件库(Paging 的最新版本)

Android 中分页功能常见的设计方法

在业务开发中由于数据信息过多,为了加速页面数据展示,提升用户体验和更高效低利用网络带宽和系统资源,分页加载成了每个 App 必有的功能之一。

在 Paging 出现之前实现分页功能基本有两种方式:

  • 为 RecycleView 添加 header 和 footer 并自行处理滑动加载等事件
  • 借助第三方开源框架处理业务逻辑(也是基于第一种方式实现的)

Google 为了统一分页加载的实现方案,以使开发者更多地专注于业务功能的实现,推出了分页加载库 Paging,Paging3 作为 Paging 组件的最新版本,比 Paging 更加便捷。

网络请求的封装与使用

首先在 build.gradle 中添加网络请求相关的配置:

1
2
3
4
5
6
7
8
9
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation("com.squareup.okhttp3:logging-interceptor:4.7.2")
// paging3 相关库
def paging_version = "3.1.1"
implementation "androidx.paging:paging-runtime:$paging_version"
testImplementation "androidx.paging:paging-common:$paging_version"
}

测试接口使用 玩Android 的开放API - 问答

新建接口文件类:

1
2
3
object BaseApi {
val BASE_URL = "https://www.wanandroid.com/"
}
1
2
3
4
interface TestApi {
@GET("wenda/list/{pageId}/json")
suspend fun getData(@Path("pageId") pageId:Int):TestDataBean
}

新建接口返回的对应的 body 实体数据:

TestDataBean.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
data class TestDataBean(
val `data`: Data,
val errorCode: Int,
val errorMsg: String
)

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

data class DataX(
val apkLink: String,
val audit: Int,
val author: String,
val canEdit: Boolean,
val chapterId: Int,
val chapterName: String,
val collect: Boolean,
val courseId: Int,
val desc: String,
val descMd: String,
val envelopePic: String,
val fresh: Boolean,
val host: String,
val id: Int,
val link: String,
val niceDate: String,
val niceShareDate: String,
val origin: String,
val prefix: String,
val projectLink: String,
val publishTime: Long,
val realSuperChapterId: Int,
val selfVisible: Int,
val shareDate: Long,
val shareUser: String,
val superChapterId: Int,
val superChapterName: String,
val tags: List<Tag>,
val title: String,
val type: Int,
val userId: Int,
val visible: Int,
val zan: Int
)

data class Tag(
val name: String,
val url: String
)

将创建 service 的过程封装起来,新建 RetrofitServiceBuilder 类,提供 createService 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
object RetrofitServiceBuilder  {

fun <T> createService(clazz:Class<T>,baseApi:String = BaseApi.BASE_URL):T?{
// 网络未连接时提示用户直接返回null
if (!NetworkUtils.isConnected(MyApplication.applicationContext())){
Toast.makeText(MyApplication.applicationContext(),"网络未连接",Toast.LENGTH_SHORT).show()
return null
}
// 添加日志拦截器
val interceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger{
override fun log(message: String) {
HttpLoggingInterceptor.Logger.DEFAULT.log(message)
}
})
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
val builder = OkHttpClient.Builder().addInterceptor(interceptor)
val retrofit:Retrofit = Retrofit.Builder()
.baseUrl(baseApi)
.client(builder.build())
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(clazz)
}
}

上述代码中处理了无网络时的提示语,并为 retrofit 添加了网络请求日志等方法。baseApi 默认为 BaseApi.BASE_URL 的值,可通过传参修改其值,在实际开发中可能还需要处理是否需要校验 Token、超时时长等参数配置。

使用 Paging3 实现网络数据的分页加载

官方推荐的最佳架构

使用 Paging 可以更高效地实现分页加载功能,Paging3 的使用完全符合官网推荐的最佳架构模式。好的架构应当满足关注点分离、持久性模型驱动页面等原则。Google 官方推荐的一种常见架构模式 MVVM 如下:

Repository 是数据仓库层,负责从 Room 或网络请求中获取数据,这里新建 TestRepository 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TestRepository {
// Flow 注意不要错
// import kotlinx.coroutines.flow.Flow
// Paging3 返回的数据类型是 Flow<PagingData<T>>,
// 其中,T 是业务中的数据类型,在这里对应的是 StudentReqData.ListBean。
fun loadStudentMessage(): Flow<PagingData<DataX>> {
return Pager(
// PagingConfig 指定了关于分页的参数配置,主要有如下属性:
// pageSize:每次加载数据量的大小(指定每页数据量大小)。
// initialLoadSize:处理加载数据量的大小,默认为 pageSize 的三倍。
// enablePlaceholders:是否启动展示位,启动后数据未加载出来之前将显示空白的展示位。
// prefetchDistance:预取距离,当数据超过这个数值时自动触发加载下一页。
config = PagingConfig(pageSize = 8),
// pagingSourceFactory 参数指定了加载分页的数据源是 TestDataSource
pagingSourceFactory = {TestDataSource()}
).flow
}
}

ViewModel 层通过调用 Repository 层获取数据,并通过 LiveData 或 Flow 将数据发射到 UI 层,UI 层感知到数据变化后将数据展示出来:

1
2
3
4
5
6
7
8
class TestViewModel(application: Application):AndroidViewModel(application)  {
// Flow 注意不要错
// import kotlinx.coroutines.flow.Flow
fun loadStudentMessage(): Flow<PagingData<DataX>> {
// cachedIn 函数的作用是将服务器返回的数据在 viewModelScope 这个作用域内进行缓存,当手机屏幕旋转时便会保存数据。
return TestRepository().loadStudentMessage().cachedIn(viewModelScope)
}
}

接下来查看 Paging3 中的数据源 PagingSource 是如何实现的

PagingSource 的定义与使用

PagingSource 是 Paging3 中的核心组件之一,用于处理数据加载逻辑,定义 TestDataSource 继承自 PagingSource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// PagingSource 有两个泛型类型:
// 1、表示页码参数的类型。2、表示每一项数据对应的实体类。
class TestDataSource : PagingSource<Int, DataX>() {
// 创建 TestApi 对应的 service 的代码:
private val testApi = RetrofitServiceBuilder.createService(TestApi::class.java)

// 复写 load 方法以提供加载数据的逻辑
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DataX> {
try {
// 通过 params.key 方法获取当前页码值,页码未定义置为 1(默认第一页)
val currentPage = params.key ?: 1
// 仓库层请求数据
// params.loadSize 方法可以获取声明的每页数据量的大小(PagingConfig-pageSize 设置)
// 在 PagingConfig 的 initialLoadSize 参数默认是 pageSize 的 3 倍,所以第一次加载数据的下标为 1~15,第二次加载的数据下标为 6~10,可以看到,这里面有部分数据是重复的。
// Google 官方对此的回复是:仅使用 params.loadSize 请求接口导致程序出现数据重复的问题是正常且符合预期的,如果想避免这个问题,在加载数据时需要将 params.loadSize 替换为业务中的 pageSize,
// 因为 Paging3 的设计理念就是不让开发者关心业务的具体实现,而是完全交给 Paging3 去处理。
// 程序通过网络请求获取数据 data,当程序请求异常时,这里使用 LoadResult.Error 抛出异常,在实际项目中则需要根据对应业务来处理。
val data = testApi?.getData(currentPage)
// Log.d("TAG","loadSize${params.loadSize}")
Log.d("TAG","当前页码$currentPage")
// 题外话:这里将总页码的 pageCount(8) 认错成了 total(162),
// 导致在刷到第 9 页时(currentPage 为 9)会持续触发此函数,导致无操作的情况下会一直加载到 total 页。
if (currentPage == 1) Log.d("TAG","总页码${data?.data?.pageCount}")

// 当前页码小于总页码时,页码加 1
val nextPage =
if (currentPage < (data?.data?.pageCount ?: 0)) {
currentPage + 1
} else {
null
}
// 上一页
val prevKey = if (currentPage > 1) {
currentPage - 1
} else {
null
}

when (data?.errorCode) {
0 -> {
data.data.datas.let {
// 正常返回的数据通过 LoadResult.Page 方法返回
return LoadResult.Page(
// 需要加载的数据
data = it, // 返回的列表数据
prevKey = prevKey, //前一页
// 加载下一页的 key,如果传 null 就说明到底了
nextKey = nextPage // 下一页
)
}
}
// 其他业务逻辑处理...
else -> {}
}
}catch (e:Exception){
return LoadResult.Error(e)
}
return LoadResult.Error(throwable = IOException())
}

override fun getRefreshKey(state: PagingState<Int, DataX>): Int? {
return null
}
}

PagingDataAdapter 的定义与使用

为了让数据显示在 RecycleView 控件上,需要为 RecycleView 绑定一个适配器。在 Paging3 分页库中同样需要为其绑定一个适配器,新建 TestPagingDataAdapter 并继承自 PagingDataAdapter:

先查看 PagingDataAdapter 的 构造方法:

1
2
3
4
5
abstract class PagingDataAdapter<T : Any, VH : RecyclerView.ViewHolder> @JvmOverloads constructor(
diffCallback: DiffUtil.ItemCallback<T>,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
workerDispatcher: CoroutineDispatcher = Dispatchers.Default
) : RecyclerView.Adapter<VH>() { ... }

PagingDataAdapter 继承自 RecyclerView.Adapter<VH>,所以 PagingDataAdapter 的功能与普通的 Adapter 无异,只是需要指定 DiffUtil.ItemCallback 参数。DiffUtil.ItemCallback 用于计算列表中两个非空项目之间差异的回调。需要注意的是,DiffUtil.ItemCallback 是 RecycleView 组件中的功能,而不是 Paging3 中的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class TestPagingDataAdapter : PagingDataAdapter<DataX,RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<DataX>(){
override fun areItemsTheSame(
oldItem: DataX,
newItem: DataX
): Boolean {
return oldItem.id == newItem.id
}

@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(
oldItem: DataX,
newItem: DataX
): Boolean {
return oldItem == newItem
}

}){
/**
* 将数据绑定到对应 xml 布局中
*/
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val mHolder = holder as TestDataViewHolder
val bean:DataX?=getItem(position)
bean?.let {
mHolder.dataBindingUtil.bean = it
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding:ItemTestDataBinding=ItemTestDataBinding.inflate(LayoutInflater.from(parent.context),parent,false)
return TestDataViewHolder(binding)
}

class TestDataViewHolder(var dataBindingUtil:ItemTestDataBinding):RecyclerView.ViewHolder(dataBindingUtil.root){

}
}

将结果显示在 UI 上

这里使用了 DataBinding 数据绑定功能:

item_test_data.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="bean"
type="com.example.littlehelper.ui.test.DataX" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{bean.author}"
android:textColor="#000000"
android:textSize="20sp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text='@{bean.title}'
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#999999" />
</LinearLayout>
</layout>

在 Activity 中调用 ViewModel 中加载数据的方法,并将数据设置给 adapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class TestActivity : AppCompatActivity() {
private val TAG = "TAG_TestActivity"
private lateinit var binding: ActivityTestBinding
private lateinit var testViewModel: TestViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTestBinding.inflate(layoutInflater)
setContentView(binding.root)

initData()
}

private fun initData() {
testViewModel = ViewModelProvider(this).get(TestViewModel::class.java)
val testPagingDataAdapter = TestPagingDataAdapter()
binding.rv.layoutManager = LinearLayoutManager(this)
binding.rv.adapter = testPagingDataAdapter

// 由于 collect 是一个挂起函数,所以这里需要在协程中操作。
// 接收到数据后,调用 PagingAdapter 的 submitData 方法,Paging3 开始工作,他会将数据显示在页面上。
lifecycleScope.launch {
try {
testViewModel.loadStudentMessage().collect{
testPagingDataAdapter.submitData(it)
}
}catch (e:Exception){
Log.d(TAG,e.toString())
}
}
}
}

效果图示例:

监听加载状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
        testPagingDataAdapter.addLoadStateListener {
when(it.refresh){
is LoadState.Loading ->{
Log.d(TAG,"正在加载")
}
is LoadState.Error ->{
Log.d(TAG,"加载错误")
}
is LoadState.NotLoading ->{
Log.d(TAG,"未加载,无错误")
}
}
}

// 日志打印:
// D/TAG_TestActivity: 正在加载
// D/TAG: 当前页码1
// D/TAG: 总页码8
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: 未加载,无错误
// D/TAG: 当前页码2
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: 未加载,无错误
// D/TAG: 当前页码3
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: 未加载,无错误
// D/TAG: 当前页码4
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: 未加载,无错误
// D/TAG: 当前页码5
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}@53c96cc

上述代码通过 PagingAdapter 的 addLoadStateListener 方法进行监听,监听参数是一个 CombinedLoadStates 类,CombinedLoadStates 类中每种状态及其对应的使用场景如下:

值类型 使用场景
refresh 数据刷新时
prepend 数据向上一页加载时
append 数据向下一页加载时
source 从 PagingSource 加载
mediator 从 RemoteMediator 加载

这里以 “refresh 数据刷新时” 为例,LoadState 的状态有三种,其状态值与含义描述如下:

状态值 含义描述
Loading 正在加载中
Error 加载错误
NotLoading 没有在加载,一般在加载前或加载完成后

程序在下翻浏览数据时,可能由于网络异常等问题导致数据加载失败,这时可在页面中隐藏一个重试按钮,当出现异常时,将按钮展示出来即可进行相应的业务处理。而 Paging3 提供了更简单的实现方式,首先定义一个底部布局,放一个重试按钮和 ProgressBar:

效果图示例:

item_test_foot.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/ll_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="正在加载数据......"
android:textSize="18sp" />
<ProgressBar
android:layout_width="20dp"
android:layout_height="20dp" />
</LinearLayout>
<Button
android:id="@+id/btn_retry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="加载失败,重新请求"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ll_loading" />
</androidx.constraintlayout.widget.ConstraintLayout>

接着定义一个 LoadStateViewHolder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ViewHolder 与底部布局绑定,通过 bindStatue 方法处理加载状态中的逻辑。
class LoadStateViewHolder(parent: ViewGroup, var retry: () -> Unit) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_test_foot, parent, false
)
) {
val itemLoadStateBindingUtil: ItemTestFootBinding = ItemTestFootBinding.bind(itemView)

fun bindStatue(loadState: LoadState) {
if (loadState is LoadState.Error) {
Log.d("TAG","----------LoadState.Error----")
itemLoadStateBindingUtil.btnRetry.visibility = View.VISIBLE
itemLoadStateBindingUtil.btnRetry.setOnClickListener {
retry()
}
}else if(loadState is LoadState.Loading){
Log.d("TAG","--------LoadState.Loading------")
itemLoadStateBindingUtil.llLoading.visibility = View.VISIBLE
}else if (loadState is LoadState.NotLoading){
Log.d("TAG","--------LoadState.NotLoading------")
}
}
}

然后创建一个 LoadStateFootAdapter 使其继承自 LoadStateAdapter,并指定 ViewHolder 为 LoadStateViewHolder,之后通过 onBindViewHolder 方法调用 bindStatue 方法:

1
2
3
4
5
6
7
8
9
class LoadStateFootAdapter(private val retry:()->Unit):LoadStateAdapter<LoadStateViewHolder>() {
override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
(holder as LoadStateViewHolder).bindStatue(loadState)
}

override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
return LoadStateViewHolder(parent,retry)
}
}

最后通过 withLoadStateFooter 方法为 adapter 添加底部状态布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
        binding.rv.adapter =
testPagingDataAdapter.withLoadStateFooter(
footer = LoadStateFootAdapter(
retry={
testPagingDataAdapter.retry()
}
)
)
// 日志打印:
// D/TAG_TestActivity: 正在加载
// D/TAG: 当前页码1
// D/TAG: 总页码8
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: 未加载,无错误
// D/TAG: --------LoadState.Loading------
// D/TAG: 当前页码2
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: 未加载,无错误
// D/TAG: --------LoadState.Loading------
// D/TAG: 当前页码3
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: 未加载,无错误
// D/TAG: --------LoadState.Loading------
// D/TAG: 当前页码4
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: 未加载,无错误
// D/TAG: --------LoadState.Loading------
// D/TAG: 当前页码5
// D/TAG_TestActivity: 未加载,无错误
// D/TAG_TestActivity: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}@53c96cc

上述代码会在按钮重试事件的回调中调用 testPagingDataAdapter.retry() 方法,retry 方法是 PagingAdapter 提供的重试方法,除此之外,还有 refresh(刷新)等方法。


原理解析

Paging3 组件的使用流程比较固定,它主要是在 Respository 中调用 PagingSource,在 ViewModel 中调用 Respository 并返回 Flow<PagingData> 到 UI 层,最后 UI 层接收数据并进行展示。

Paging 库如何契合应用架构的示例图:

接下来看看 Paging3 是如何做到自动加载更多内容的,从 PagingDataAdapter 的 getItem 方法说起:

PagingDataAdapter.kt
1
2
// 这里的 getItem 调用了 differ.getItem 方法,differ 是一个 AsyncPagingDataDiffer 对象。
protected fun getItem(@IntRange(from = 0) position: Int) = differ.getItem(position)

differ.getItem 方法如下:

AsyncPagingDataDiffer.kt
1
2
3
4
5
6
7
8
9
fun getItem(@IntRange(from = 0) index: Int): T? {
try {
inGetItem = true
// 调用 differBase[index] 方法
return differBase[index]
} finally {
inGetItem = false
}
}

differBase 是 PagingDataDiffer 对象,查看 PagingDataDiffer 的 get 方法:

PagingDataDiffer.kt
1
2
3
4
5
6
7
public operator fun get(@IntRange(from = 0) index: Int): T? {
lastAccessedIndexUnfulfilled = true
lastAccessedIndex = index

receiver?.accessHint(presenter.accessHintForPresenterIndex(index))
return presenter.get(index)
}

receiver 实例是 UiReceiver 接口,它里面包含了 accessHint 以及供使用的 retry 和 refresh 方法:

UiReceiver.kt
1
2
3
4
5
internal interface UiReceiver {
fun accessHint(viewportHint: ViewportHint)
fun retry()
fun refresh()
}

最终 accessHint 会调用 PageFetcherSnapshot 类的 accessHint 方法:

PageFetcher.kt
1
2
3
4
5
6
7
8
9
10
inner class PagerUiReceiver<Key : Any, Value : Any> constructor(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val pageFetcherSnapshot: PageFetcherSnapshot<Key, Value>,
private val retryEventBus: ConflatedEventBus<Unit>
) : UiReceiver {
override fun accessHint(viewportHint: ViewportHint) {
pageFetcherSnapshot.accessHint(viewportHint)
}
...
}
PageFetcherSnapshot.kt
1
2
3
fun accessHint(viewportHint: ViewportHint) {
hintHandler.processHint(viewportHint)
}

查看 HintHandler:

HintHandler.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private inner class HintFlow {
var value: ViewportHint? = null
set(value) {
field = value
if (value != null) {
_flow.tryEmit(value)
}
}
private val _flow = MutableSharedFlow<ViewportHint>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val flow: Flow<ViewportHint>
get() = _flow
}

_flow 是 MutableSharedFlow,它通过 tryEmit 方法发送数据。接下来 hintHandler 查看接收数据的地方:

PageFetcherSnapshot.kt
1
2
3
4
5
6
7
8
    hintHandler.hintFor(loadType)
.drop(if (generationId == 0) 0 else 1)
.map { hint -> GenerationalViewportHint(generationId, hint) }
}.simpleRunningReduce { previous, next ->
if (next.shouldPrioritizeOver(previous, loadType)) next else previous
}.conflate().collect { generationalHint ->
doLoad(loadType, generationalHint)
}

最终走到 doLoad 方法中:

PageFetcherSnapshot.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private suspend fun doLoad(
loadType: LoadType,
generationalHint: GenerationalViewportHint
) {
...
var loadKey: Key? = stateHolder.withLock { state ->
state.nextLoadKeyOrNull(
loadType,
generationalHint.generationId,
generationalHint.hint.presentedItemsBeyondAnchor(loadType) + itemsLoaded,
)?.also { state.setLoading(loadType) }
}

var endOfPaginationReached = false
loop@ while (loadKey != null) {
val params = loadParams(loadType, loadKey)
val result: LoadResult<Key, Value> = pagingSource.load(params)
when (result) {
is Page<Key, Value> -> {
// First, check for common error case where the same key is re-used to load
// new pages, often resulting in infinite loops.
val nextKey = when (loadType) {
PREPEND -> result.prevKey
APPEND -> result.nextKey
else -> throw IllegalArgumentException(
"Use doInitialLoad for LoadType == REFRESH"
)
}
...
}
...
}
}

当 loadKey 不为 null 时,会调用 pagingSource 的 load 方法,loadKey 是通过 stateHolder 的 nextLoadKeyOrNull 方法获取的:

PageFetcherSnapshot.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private fun PageFetcherSnapshotState<Key, Value>.nextLoadKeyOrNull(
loadType: LoadType,
generationId: Int,
presentedItemsBeyondAnchor: Int
): Key? {
if (generationId != generationId(loadType)) return null
if (sourceLoadStates.get(loadType) is Error) return null

if (presentedItemsBeyondAnchor >= config.prefetchDistance) return null

return if (loadType == PREPEND) {
pages.first().prevKey
} else {
pages.last().nextKey
}
}

当最后一个 Item 的距离小于 prefetchDistance(即预加载距离)时,会返回 nextKey 开始加载下一页,这样 Paging3 就实现了用户无感知分页加载的功能。


备注

参考资料

《Android Jetpack开发 原理解析与应用实战》

Paging 库概览

欢迎关注微信公众号:非也缘也