Android Jetpack之Paging初探

Paging是Google 2018 IO大会最新发布的Jetpack中的一个组件,主要用于大数据的分页加载,这篇文章就来探索一下关于Paging的简单使用。

Paging介绍

Paging主要由三个部分组成:DataSource PageList PageListAdapter

DataSource

DataSource<Key, Value>从字面意思理解是一个数据源,其中key对应加载数据的条件信息,Value对应加载数据的实体类。
DataSource是一个抽象类,但是我们不能直接继承它实现它的子类。但是Paging库里提供了它的三个子类供我们继承用于不同场景的实现:

  • PageKeyedDataSource<Key, Value> :适用于目标数据根据页信息请求数据的场景,即Key 字段是页相关的信息。比如请求的数据的参数中包含类似next/previous页数的信息。
  • ItemKeyedDataSource<Key, Value> :适用于目标数据的加载依赖特定item的信息, 即Key字段包含的是Item中的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时,该场景多出现于论坛类应用评论信息的请求。
  • PositionalDataSource<T>:适用于目标数据总数固定,通过特定的位置加载数据,这里Key是Integer类型的位置信息,T即Value。 比如从数据库中的1200条开始加在20条数据。

PageList

PageList是一个List的子类,支持所有List的操作,除此之外它主要有五个成员:
mMainThreadExecutor: 一个主线程的Excutor, 用于将结果post到主线程。

mBackgroundThreadExecutor: 后台线程的Excutor.

BoundaryCallback:加载Datasource中的数据加载到边界时的回调.

Config: 配置PagedList从Datasource加载数据的方式, 其中包含以下属性:

  • pageSize:设置每页加载的数量
  • prefetchDistance:预加载的数量,默认为pagesize
  • initialLoadSizeHint:初始化数据时加载的数量,默认为pageSize*3
  • enablePlaceholders:当item为null是否使用PlaceHolder展示

PagedStorage<T>: 用于存储加载到的数据,它是真正的蓄水池所在,它包含一个ArrayList<List> 对象mPages,按页存储数据。

PagedList会从Datasource中加载数据,更准确的说是通过Datasource加载数据, 通过Config的配置,可以设置一次加载的数量以及预加载的数量。 除此之外,PagedList还可以向RecyclerView.Adapter发送更新的信号,驱动UI的刷新。

PagedListAdapter

PagedListAdapte是RecyclerView.Adapter的实现,用于展示PagedList的数据。它本身实现的更多是Adapter的功能,但是它有一个小伙伴PagedListAdapterHelper<T>, PagedListAdapterHelper会负责监听PagedList的更新, Item数量的统计等功能。这样当PagedList中新一页的数据加载完成时, PagedAdapte就会发出加载完成的信号,通知RecyclerView刷新,这样就省略了每次loading后手动调一次notifyDataChanged().

除此之外,当数据源变动产生新的PagedList,PagedAdapter会在后台线程中比较前后两个PagedList的差异,然后调用notifyItem…()方法更新RecyclerView.这一过程依赖它的另一个小伙伴ListAdapterConfig, ListAdapterConfig负责主线程和后台线程的调度以及DiffCallback的管理,DiffCallback的接口实现中定义比较的规则,比较的工作则是由PagedStorageDiffHelper来完成。

上述关于Paging的介绍参考Android.Arch.Paging: 分页加载的新选项

Paging的使用

导入Paging库:

1
2
3
4
5
6
7
8
9
10
11
dependencies {
def paging_version = "1.0.0"

implementation "android.arch.paging:runtime:$paging_version"

// alternatively - without Android dependencies for testing
testImplementation "android.arch.paging:common:$paging_version"

// optional - RxJava support, currently in release candidate
implementation "android.arch.paging:rxjava2:1.0.0-rc1"
}

接下来我们看看关于Paging的具体使用

1、创建DataRepository

首先创建DataRepository用于数据的加载,因为这里我们主要实验Paging的使用,所以这里我们就模拟一个本地数据就好了,代码如下:

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
//数据类
data class DataBean(var id:Long, var name:String)

class DataRepository : AnkoLogger {

private val data = ArrayList<DataBean>()

init {
for (i in 0..100) {
val bean = DataBean(i.toLong(), "name $i")
data.add(bean)
}

}

fun loadData(size: Int): List<DataBean> {
info(" init load $size")
return data.subList(0, size)
}

fun loadData(index: Int, size: Int): List<DataBean>? {
info(" loadData $index $size")

if (index >= data.size - 1 || index < 1) {
return null
}

if (index + size > data.size) {
return data.subList(index + 1, data.size)
}
return data.subList(index + 1, index + size)
}

fun loadPageData(page: Int, size: Int): List<DataBean>? {
info(" loadPageData $page $size")

val totalPage = if (data.size % size == 0) {
data.size / size
} else {
data.size / size + 1
}

if (page > totalPage || page < 1) {
return null
}

if (page == totalPage) {
return data.subList((page - 1) * size, data.size)
}
return data.subList((page - 1) * size, page * size)
}
}

DataRepository初始化的时候我们对data初始化101条数据进去,然后提供两个loadData和一个loadData方法,第一个loadData用于初始加载数据,第二个是根据index加载数据,loadPageData是分页加载数据

2、自定义DataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CustomPageDataSource(val repository: DataRepository) : PageKeyedDataSource<Int, DataBean>(),AnkoLogger {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, DataBean>) {
info("loadInitial size : ${params.requestedLoadSize} ")
val data = repository.loadData(params.requestedLoadSize)
callback.onResult(data, null, 2)
}

override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, DataBean>) {
info("loadAfter size : ${params.requestedLoadSize} page:${params.key}")
val data = repository.loadPageData(params.key,params.requestedLoadSize)
data?.let {
callback.onResult(data, params.key + 1)
}
}

override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, DataBean>) {
info("loadBefore size : ${params.requestedLoadSize} page:${params.key}")
val data = repository.loadPageData(params.key,params.requestedLoadSize)
data?.let {
callback.onResult(data, params.key - 1)
}
}

}

CustomItemDataSource继承自PageKeyedDataSource,实现了loadInitialloadAfterloadBefore方法,
其中

  • loadInitial 初始加载数据
  • loadAfter 向后分页加载数据
  • loadBefore 向前分页加载数据

这三个方法都有两个参数,一个params和一个callback

其中params包装了分页加载的参数,loadInitial中的params为LoadInitialParams包含了requestedLoadSizeplaceholdersEnabled两个属性,requestedLoadSize为加载的数据量,placeholdersEnabled是是否显示占位及当数据为null时显示占位的view

loadBeforeloadAfter中的params为LoadParams包含了keyrequestedLoadSize,key即为DataSource<Key, Value>中的key,在这里即为页数

callback为数据加载完成的回调,loadInitial中调用调用DataRepository加载数据,然后调用callback.onResult告诉调用者数据加载完成

1
2
public abstract void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
@Nullable Key nextPageKey);

onResult有三个参数,第一个为数据,后面两个即为上一页和下一页

如果我们当前页为第一页即没有上一页,则上一页为null,下一页为2,此时加载的时候会加载当前页和调用loadAfter加载第二页,但不会调用loadBefore因为没有上一页,即previousPageKey为null不会加载上一页

如果我们初始加载的是第三页,则上一页是2,下一页是4,此时加载的时候会加载当前页和调用loadAfter加载第4页,调用loadBefore加载第二页

分页加载的时候会将previousPageKeynextPageKey传递到loadAfterloadBefore中的params.key

loadAfterloadBefore中的params中的key即为我们要加载页的数据,加载完后回调中告知下一次加载数据页数+1或者-1

接下来创建DataSourceFactory类,创建PageList的使用用得着

1
2
3
4
5
class CustomPageDataSourceFactory(val repository: DataRepository) : DataSource.Factory<Int, DataBean>() {
override fun create(): DataSource<Int, DataBean> {
return CustomPageDataSource(repository)
}
}

很简单,不多说

3、创建PageList

Paging库中提供了LivePagedListBuilder用于创建PageList,LivePagedListBuilder有两个构造方法:

1
2
3
4
public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
@NonNull PagedList.Config config)
public LivePagedListBuilder(@NonNull DataSource.Factory<Key, Value> dataSourceFactory,
int pageSize)

第一个参数为dataSourceFactory,即我们上面创建的CustomPageDataSourceFactory,第二个参数一个是PagedList.Config一个是pageSize,pagesize好理解即分页加载每页的数据条数,PagedList.Config我们前面介绍PagedList的时候也介绍过了。
LivePagedListBuilder.build()返回LiveData<PagedList<Value>>一个LiveData包裹的PageList,便于观察数据更新刷新界面。

1
2
3
4
5
val data = LivePagedListBuilder(CustomPageDataSourceFactory(DataRepository()), PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(true)
.setInitialLoadSizeHint(20)
.build()).build()

4、创建PageAdapter

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
class CustomAdapter : PagedListAdapter<DataBean, CustomViewHolder>(DIFF_CALLBACK) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder =
CustomViewHolder(parent)

override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
holder.bindTo(getItem(position))
}

companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<DataBean>() {
override fun areItemsTheSame(oldConcert: DataBean,
newConcert: DataBean): Boolean =
oldConcert.id == newConcert.id

override fun areContentsTheSame(oldConcert: DataBean,
newConcert: DataBean): Boolean =
oldConcert == newConcert
}
}

}


class CustomViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)) {
private val nameView = itemView.findViewById<TextView>(R.id.name)
var item: DataBean? = null

fun bindTo(item: DataBean?) {
this.item = item
nameView.text = item?.name
}
}

adapter继承PagedListAdapter,它的构造函数需要一个DiffUtil.ItemCallback,它的作用是判断两个item的数据是否相等

5、使用

前面我们准备工作都做完了,Paging的三大部分我们都实现了,接下来看看怎么使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val adapter = CustomAdapter()
recycle.adapter = adapter

val data = LivePagedListBuilder(CustomPageDataSourceFactory(DataRepository()), PagedList.Config.Builder()
.setPageSize(20)
.setEnablePlaceholders(true)
.setInitialLoadSizeHint(20)
.build()).build()

data.observe(this, Observer {
adapter.submitList(it)
})
}
}

布局里就一个RecyclerView就不贴代码了,因为data是一个LiveData所以这里给它设置一个观察,当数据变动时调用adapter.submitList刷新数据,submitListPagedListAdapter的方法,它里面会去检查新数据是否跟旧数据是否相同,有兴趣的可以去看看源码。
看看效果:

日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
05-29 17:09:56.870 17080-17119/? I/CustomPageDataSource: loadInitial size : 20 
05-29 17:09:56.870 17080-17119/? I/DataRepository: init load 20
05-29 17:09:56.912 17080-17119/? I/CustomPageDataSource: loadAfter size : 20 page:2
05-29 17:09:56.912 17080-17119/? I/DataRepository: loadPageData 2 20
05-29 17:09:58.049 17080-17136/? I/CustomPageDataSource: loadAfter size : 20 page:3
05-29 17:09:58.049 17080-17136/? I/DataRepository: loadPageData 3 20
05-29 17:09:58.454 17080-17119/? I/CustomPageDataSource: loadAfter size : 20 page:4
05-29 17:09:58.454 17080-17119/? I/DataRepository: loadPageData 4 20
05-29 17:09:58.789 17080-17136/? I/CustomPageDataSource: loadAfter size : 20 page:5
05-29 17:09:58.789 17080-17136/? I/DataRepository: loadPageData 5 20
05-29 17:09:59.106 17080-17119/? I/CustomPageDataSource: loadAfter size : 20 page:6
05-29 17:09:59.106 17080-17119/? I/DataRepository: loadPageData 6 20
05-29 17:09:59.140 17080-17136/? I/CustomPageDataSource: loadAfter size : 20 page:7
05-29 17:09:59.140 17080-17136/? I/DataRepository: loadPageData 7 20

说明确实是分页加载的数据。

上面我们用的是继承PageKeyedDataSource,继承ItemKeyedDataSource其实也差不多:

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
class CustomItemDataSource(val repository: DataRepository) : ItemKeyedDataSource<Long, DataBean>(),AnkoLogger {

override fun loadInitial(params: LoadInitialParams<Long>, callback: LoadInitialCallback<DataBean>) {
info(" size : ${params.requestedLoadSize} ")
val data = repository.loadData(params.requestedLoadSize)
callback.onResult(data)
}

override fun loadAfter(params: LoadParams<Long>, callback: LoadCallback<DataBean>) {
info("size : ${params.requestedLoadSize} key:${params.key}")
val data = repository.loadData(params.key.toInt(),params.requestedLoadSize)
data?.let {
callback.onResult(data)
}
}

override fun loadBefore(params: LoadParams<Long>, callback: LoadCallback<DataBean>) {
info("loadBefore ${params.key} ${params.requestedLoadSize}")
val data = repository.loadData(params.key.toInt(),params.requestedLoadSize)
data?.let {
callback.onResult(data)
}
}

override fun getKey(item: DataBean): Long {
return item.id
}

}

PageKeyedDataSource多了一个getKey方法,因为ItemKeyedDataSource根据item的信息分页加载数据的,比如这里使用排序id去加载,原理一样只是加载数据的方式不一样。

好了Paging的初探就到这里了,更多关于Paging的介绍请看官方文档:Paging library

坚持原创技术分享,您的支持将鼓励我继续创作!