jetpack之Paging的使用方法

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
ItemKeyedDataSourcePageKeyedDataSource很相似,不同的是,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 更新数据

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值