目录
jetpack的paging是用来处理分页数据加载的一环
本篇介绍jetpack的paging的基本使用方法
1.配置信息
def paging_version = "2.1.1"
implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
// alternatively - without Android dependencies for testing
testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
// optional - RxJava support
implementation "androidx.paging:paging-rxjava2:$paging_version" // For Ko
选择配置,配置第一个就已经满足基本要求
2.使用方式
2.1 定义数据源对象
就是定义我们数据对象,比如列表数据源
data class Person(var name: String, var positon: Int) {
override fun equals(other: Any?): Boolean {
other as Person
return name?.equals(other?.name) && positon == other.positon;
}
override fun hashCode(): Int {
return "$name $positon".hashCode()
}
}
我这里重写了比较的方法,其实这里写不写都是可以的
2.2 定义DataSource
这里常用的有三种DataSource
1.PositionalDataSource :这是position分页器,就是和每页数据大小无关,是按照位置进行下一页加载的,加载下一页的方法中会告诉你当前的位置,但因为设置时候会默认需要设置每页数据大小,也就是说除了首页,其他页的数据大小基本都是一致的,所以加载下一页的起始位置可能不固定,但是每页大小一般是固定的
2.PageKeyedDataSource :这个是page分页器,也是我们常用的分页,当需要指定当期的页数,和每页条固定的分页模式,这个比较适合;在每次请求数据结果上, 可以指定下一次的页码属性,如果没有下一页就指定为null即可
3.ItemKeyedDataSource :这个是Item分页器,和PageKeyedDataSource功能差不多,但比之更加灵活,因为这个多了Key的判断方法,可以根据上一页的最后一条数据的属性来绝对当前页的操作,而不仅限于固定的Key格式
我这里先定义两个模拟数据填充的方法
const val pageSize = 10;
const val TAG_1234 = "1234"
fun fetchResult(start: Int, size: Int): List<Person> {
val end = start + size - 1
val list = mutableListOf<Person>()
for (i in start..end) {
val person = Person("Name ->>> $i", i)
list.add(person)
}
return list
}
fun fetchResultByPage(page: Int, size: Int): List<Person> {
val start = (page - 1) * pageSize
val end = start + size - 1
val list = mutableListOf<Person>()
for (i in start..end) {
val person = Person("Name ->>> $i", i)
list.add(person)
}
return list
}
第一个方法是按照起始位置添加指定大小元素,第二方法是根据页数和指定的每页固定大小算出起始位置,然后添加指定大小的元素
2.2.1 PositionalDataSource
class PersonDataSource : PositionalDataSource<Person>() {
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Person>) {
Log.e(TAG_1234, "loadRange ${params.startPosition}")
GlobalScope.launch(Dispatchers.Main) {
val result = GlobalScope.async(Dispatchers.IO) {
delay(1000)
return@async fetchResult(params.startPosition, params.loadSize)
}.await()
if (params.startPosition < 50){
callback.onResult(result)
}
}
}
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Person>) {
Log.e(TAG_1234, "onLoadInitial ${params.requestedLoadSize}")
GlobalScope.launch(Dispatchers.Main) {
val result = GlobalScope.async(Dispatchers.IO) {
delay(1000)
return@async fetchResult(0,params.requestedLoadSize)
}.await()
callback.onResult(result, 0)
}
}
}
这里我定义了50的大小限制,超过50的不会再返回数据,然后打印了每次加载的方法
在loadInitial方法中初始化了一个10大小的集合,索引从0开始,就是实际会初始一个索引从0-9的集合
在lloadInitial方法中lLoadInitialCallback有两个填充的方法
public abstract static class LoadInitialCallback<T> {
public abstract void onResult(@NonNull List<T> data, int position, int totalCount);
public abstract void onResult(@NonNull List<T> data, int position);
}
三个参数分别是:
1… 数据源,就是我们请求的第一页的数据
2…数据源填充的起始位置,一般填的是0,如果你填其他的会有很神奇的现象
3…数据总长度,如果是已知总长度的可以填,否则填了会有问题;因为这个是总长度,后续如果发现实际长度不匹配,则超过的不会显示,不足的会以null形式填充数据,始终保持长度不变,UI显示会出问题
上面我填的0,然后没用总长度,这里得提一下,没用总长度必须设置相应的config属性,这里先列出来
val config = PagedList.Config.Builder()
.setPageSize(pageSize)
.setInitialLoadSizeHint(pageSize)
.setEnablePlaceholders(false)
.setPrefetchDistance(5)
.build()
setPageSize是设置每页大小,必填
setInitialLoadSizeHint是设置初始化的大小,这个值可以从LoadInitialParams的requestedLoadSize中获取到,paging的分页会以实际数据量的大小计算,这里设的再打,如果数据源没那么多,那么就以实际数据大小计算position值
setEnablePlaceholders这个是设置是否有占位符,就是上面提到的,如果设置totalCount,不足的会以null占位,不设置则不需要,并且一定要把这个值设成false
setPrefetchDistance表示距离底部多少item时候开始去加载下一页,就是比如当前页有10条数据都在屏幕外,当滑进屏幕5条时候,就会请求下一页,类似预加载
然后打印的结果是,可见loadRange的startPosition从当期的数据数往后叠加
E/1234: onLoadInitial 10
E/1234: loadRange 10
E/1234: loadRange 20
E/1234: loadRange 30
E/1234: loadRange 40
E/1234: loadRange 50
之所以说上面说页数基本固定,也是可以不固定,就算即使你设置了每页个数, 但数据并不这么想
比如,比如我把每页数据加个随机数
return@async fetchResult(params.startPosition, params.loadSize - (Math.random()*5).toInt())
打印结果是
E/1234: onLoadInitial 10
E/1234: loadRange 10
E/1234: loadRange 17
E/1234: loadRange 26
E/1234: loadRange 36
E/1234: loadRange 42
E/1234: loadRange 48
E/1234: loadRange 55
可以看出即便每页都是随机值,仍然可以保证数据的正确性
上面提到如果把初始化方法的起始位置设置成其他值会有问题,比如我们设置成3
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Person>) {
......
callback.onResult(result, 3)
}
打印结果是
E/1234: onLoadInitial 10
E/1234: loadRange 0
E/1234: loadRange 13
E/1234: loadRange 23
E/1234: loadRange 33
E/1234: loadRange 43
可见,数据会从3-12位置显示的是初始化的值,0-2和13-…之后的值都会走loadRange方法,这里得注意
2.2.2 PageKeyedDataSource
public abstract class PageKeyedDataSource<Key, Value> extends ContiguousDataSource<Key, Value> {...}
这个类有两个泛型,一个Key和一个Value,Value对应我们的数据类型,Key则可以认为是一个标记,下一页会根据这个标记来计算和拉取数据,当传递null时候则表示没有下一页了
同样我们的例子可以改造为
class PersonDataPageKeySource : PageKeyedDataSource<Int, Person>() {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Person>) {
Log.e(TAG_1234,"loadInitial ${params.requestedLoadSize}")
GlobalScope.launch(Dispatchers.Main) {
val result = GlobalScope.async(Dispatchers.IO) {
delay(1000)
return@async fetchResult(0, params.requestedLoadSize)
}.await()
callback.onResult(result, null, 2)
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Person>) {
Log.e(TAG_1234,"loadAfter ${params.key}")
GlobalScope.launch(Dispatchers.Main) {
val result = GlobalScope.async(Dispatchers.IO) {
delay(1000)
if (params.key < 5) {
return@async fetchResultByPage(params.key, params.requestedLoadSize)
} else {
return@async fetchResultByPage(params.key, 5)
}
}.await()
if (result.size < params.requestedLoadSize) {
callback.onResult(result, null)
} else {
callback.onResult(result, params.key + 1)
}
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Person>) {
// Log.e(TAG_1234,"loadBefore ${params.key}")
// val result =fetchResultByPage(10, pageSize)
// callback.onResult(result, params.key - 1)
}
我们这里用Interger类型来标记Key做分页,当然也可以定义其他类型
然后我在loadInitial方法中初始化了10条数据,然后调用了callback.onResult(result, null, 2)
在loadAfter中我模拟了前4页全数据,第五页只有5条数据;然后在回调中指定全数据是key+1,否则直接传null终止
同样初始化回调有两个方法
public abstract static class LoadInitialCallback<Key, Value> {
public abstract void onResult(@NonNull List<Value> data, int position, int totalCount,
@Nullable Key previousPageKey, @Nullable Key nextPageKey);
public abstract void onResult(@NonNull List<Value> data, @Nullable Key previousPageKey,
@Nullable Key nextPageKey);
}
第一个方法和上面PositionalDataSource类似,主要是后面两个参数,和第二个方法中都有这两个参数
这两个参数指定了上一页的Key和下一页的Key,有点类似于双向链表,但我们一般只关心下一页加载,也就是第二个值,第一个值一般设置成null
比如上面我假定了当前页是1,然后初始化了10条数据,指定下一页是2
然后加载下一页期间,如果数据条数和需求的10条相同,则继续指定下一个key+1
打印的结果是
E/1234: loadInitial 10
E/1234: loadAfter 2
E/1234: loadAfter 3
E/1234: loadAfter 4
E/1234: loadAfter 5
可见,每次加载下一页的Key就是我们指定的值,当为空的时候,后续方法就不再回调
这里只是为了方便使用int类型,使用String,对象什么的作为Key也是可以的
2.2.3 ItemKeyedDataSource
class PersonDataItemKeySource : ItemKeyedDataSource<Int, Person>() {
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Person>) {
Log.e(TAG_1234, "loadInitial")
GlobalScope.launch(Dispatchers.Main) {
val result = GlobalScope.async(Dispatchers.IO) {
delay(1000)
return@async fetchResult(0, params.requestedLoadSize)
}.await()
callback.onResult(result)
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Person>) {
Log.e(TAG_1234, "loadAfter ${params.key}")
GlobalScope.launch(Dispatchers.Main) {
val result = GlobalScope.async(Dispatchers.IO) {
delay(1000)
if (params.key < 50) {
return@async fetchResult(params.key + 1, params.requestedLoadSize)
} else {
return@async mutableListOf<Person>()
}
}.await()
callback.onResult(result)
}
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Person>) {
// Log.e(TAG_1234, "loadBefore ${params.key}")
}
override fun getKey(item: Person): Int {
return item.positon
}
}
这里多了一个getKey方法,制定了Key的生成规则,因为我们制定了泛型Integer,所以这里会最终按int的规则生成Key
ItemKeyedDataSource和PageKeyedDataSource很相似,不同的是,ItemKeyedDataSource不需要指定下一页的Key属性,会按照上一页最后一条数据的属性生成一个Key,然后把这个Key交付我们处理
比如上面我们指定是Person的position属性作为我们Key,打印结果是
E/1234: loadInitial
E/1234: loadAfter 9
E/1234: loadAfter 19
E/1234: loadAfter 29
E/1234: loadAfter 39
E/1234: loadAfter 49
E/1234: loadAfter 59
可见,每次传递的都是上一页最后一条的数据属性
2.3 定义ViewModel
class PersonViewModel : ViewModel {
private var liveData: LiveData<PagedList<Person>>
constructor() {
val factory = PersonFactory()
val config = PagedList.Config.Builder()
.setPageSize(pageSize)
.setInitialLoadSizeHint(pageSize)
.setEnablePlaceholders(false)
.setPrefetchDistance(5)
.build();
liveData = LivePagedListBuilder<Int, Person>(factory, config).build()
}
fun getPersonLiveData(): LiveData<PagedList<Person>> {
return liveData
}
inner class PersonFactory : DataSource.Factory<Int, Person>() {
override fun create(): DataSource<Int, Person> {
// return PersonDataSource()
// return PersonDataPageKeySource()
return PersonDataItemKeySource()
}
}
}
这里需要先定义一个DataSource.Factory,然后数据LiveData指定类型是LiveData<PagedList<?>>这里面传自己的数据类型
配置信息上面已经讲过了,这里就不多描述了
2.4 使用方法
我这里定义了一个列表的布局Item
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_msg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:layout_marginLeft="10dp"
android:text="@{person.name,default=abcd}"
/>
</LinearLayout>
<data class=".PagingDataBinding">
<variable
name="person"
type="com.example.jattpack.paging.Person" />
</data>
</layout>
然后定义了一个适配器
class PaginAdapter : PagedListAdapter<Person, PaginAdapter.MViewHolder> {
constructor() : super(MDiffCallBack())
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.layout_paging_recycler_test, parent, false)
return MViewHolder(itemView)
}
override fun onBindViewHolder(holder: MViewHolder, position: Int) {
val person = getItem(position)
person?.apply {
holder.dataBinding?.person = person
holder.dataBinding?.executePendingBindings()
}
}
inner class MViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val dataBinding: PagingDataBinding?
init {
dataBinding = DataBindingUtil.bind(itemView)
}
}
class MDiffCallBack : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.name == (newItem.name) && oldItem.positon == newItem.positon;
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.name == newItem.name;
}
}
}
注意这里的适配器得继承 PagedListAdapter,泛型需要额外传入一数据类型,同时别忘了在构造方法中要传入一个DiffUtil.ItemCallback
最后在Activity里面调用就可以了
class PagingActivity : AppCompatActivity(), Observer<PagedList<Person>> {
private lateinit var viewModel: PersonViewModel
private val adapter = PaginAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_paging)
viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(PersonViewModel::class.java)
viewModel.getPersonLiveData().observe(this, this)
recyclerview.layoutManager = LinearLayoutManager(this)
recyclerview.adapter = adapter
}
override fun onChanged(t: PagedList<Person>?) {
adapter.submitList(t)
}
}
注意更新方法用adapter.submitList方式提交
3.总结
1.定义数据类型
2.定义资源处理方式DataSoure,这里分三种:根据position定位分页,根据Key定位分页,根据上一页最后一个Item属性生成的Key定位分页
3.定义DataSource.Factory,在create方法中返回我们刚定义的DataSource对象
4.定义LiveData,类型定义为LiveData<PagedList<?>> 传入自己的类型;定义配置,并联合上面创建的factory,通过LivePagedListBuilder生成
5.定义适配器,指定继承PagedListAdapter,这个adapter其实也是继承RecyclerView的Adapter的
6.监听数据变动,在变动方法中使用 adapter.submitList 更新数据