Paging 3 的初步使用

7,044 阅读2分钟

Paging 3 基于 RecyclerView 提供了 1 个分页库,为用户处理了翻页、加载更多、刷新等多种功能,可以有效优化代码中这部分的逻辑。

1. 引用依赖项

implementation "androidx.paging:paging-runtime:3.0.0-alpha02"

详见 Paging 3 library overview

2. 加载数据源

2.1 Room

在定义 Dao 接口的 Query 语句时,返回类型要使用 PagingSource 类型,第 2 个参数为表的数据结构。同时不需要在 Query 里指定页数和每页展示数量,页数由 PagingSource 来控制,每页数量页在 PagingConfig 中定义。

@Dao
interface CheeseDao {
    @Query("SELECT * FROM Cheese ORDER BY name COLLATE NOCASE ASC")
    fun allCheesesByName(): PagingSource<Int, Cheese>

    @Insert
    fun insert(cheese: Cheese)

    @Delete
    fun delete(cheese: Cheese)
}

使用 Room 有一个好处是,如果通过 insert()delete() 等方法修改了 Room 里的数据,不需要额外处理就会即时反应在 PagingSource 里,界面上展示的数据会相应变化。

2.2 自定义数据源

如果不是直接使用 Room 的数据,而是使用源自其他地方的数据,比如网络数据,就需要自定义 PagingSource 了,创建方式如下:

class PageKeyedSubredditPagingSource(
        private val redditApi: RedditApi,
        private val subredditName: String
) : PagingSource<String, RedditPost>() {
    override suspend fun load(params: LoadParams<String>): LoadResult<String, RedditPost> {
        return try {
            val data = redditApi.getTop(
                    subreddit = subredditName,
                    // key 类的类型为 PagingSource<Key : Any, Value : Any> 里的 key ,具体值由用户自行定义。首次调用时为 null
                    after = if (params is Append) params.key else null,
                    before = if (params is Prepend) params.key else null,
                    limit = params.loadSize // loadSize 为请求数据量
            ).data

            Page(
                    data = data.children.map { it.data },
                    prevKey = data.before,
                    nextKey = data.after
            )
        } catch (e: IOException) {
            LoadResult.Error(e)
        } catch (e: HttpException) {
            LoadResult.Error(e)
        }
    }
}

3. 构造 Pager 和 PagingData

val allCheeses: Flow<PagingData<Cheese>> = Pager(
    PagingConfig(
        // 每页显示的数据的大小。对应 PagingSource 里 LoadParams.loadSize
        pageSize = 20,

        // 预刷新的距离,距离最后一个 item 多远时加载数据
        prefetchDistance = 3,

        // 初始化加载数量,默认为 pageSize * 3
        initialLoadSize = 60,

        // 一次应在内存中保存的最大数据
        maxSize = 200
    )
) {
    // 数据源,要求返回的是 PagingSource 类型对象
    dao.allCheesesByName()
}.flow // 最后构造的和外部交互对象,有 flow 和 liveData 两种

4. 构造自定义 PagingDataAdapter

class CheeseAdapter : PagingDataAdapter<Cheese, CheeseViewHolder>(diffCallback) {
    override fun onBindViewHolder(holder: CheeseViewHolder, position: Int) {
        holder.bindTo(getItem(position))
    }

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

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<Cheese>() {
            override fun areItemsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
                    oldItem.id == newItem.id
            override fun areContentsTheSame(oldItem: Cheese, newItem: Cheese): Boolean =
                    oldItem == newItem
        }
    }
}

PagingDataAdapter 在构造方法中只需要传入 DiffUtil.ItemCallback 对象,不需要定义和传入数据。后续会同 PagingData 关联,使用 PagingData 的数据。如果需要获取某个位置处的数据,在类里使用 getItem() 方法即可。

同时在自定义的 PagingDataAdapter 里只需要实现 onBindViewHolder()onCreateViewHolder() 方法即可。

5. 关联数据

val adapter = CheeseAdapter()
recyclerView.adapter = adapter

lifecycleScope.launch {
    viewModel.allCheeses.collectLatest { adapter.submitData(it) }
}

参考文章:

Android Jetpack 分页库概览

Jetpack 成员 Paging3 实践以及源码分析(一)

JetPack系列 Paging 3.0学习