1. 什么是Paging3
Paging3是谷歌开发的一个针对Android系统的库。Paging3可以让应用无缝的加载并展示从本地或者服务器中获取的数据。
对比Paging2,Paging3有以下5点的更新。
PagingSource
的实现更加简单。- 支持
Flow
。 - 增加请求数据时状态的回调,
- 支持设置
Header
和Footer
。 - 支持多种方式请求数据,比如网络请求和数据库请求。
关于Paging2的教程可以看这篇:juejin.cn/post/684490…
2. Paging3的架构
上图所示的是利用Paging3的应用的架构。在Repository
层获取数据,然后传给ViewModel
层中的Pager,最后PagingDataAdaper
从ViewModel
中获取Flow
数据并展示。
PagingSource
: 是一个单一的数据源,PagingSource可以从本地或服务器中加载数据。RemoteMediator
: 也是一个单一的数据源,在PagingSource中没有数据的时候,会使用RemoteMediator的数据。如果数据既存在于数据库,又存在于服务器中,更多的情况PagingSource用于数据库请求,RemoteMediator用于服务器请求。PagingData
: 单次分页数据的容器。Pager
: 用来构建Flow的类,实现数据加载完成的回调。PagingDataAdapter
: 分页加载数据的RecyclerView的适配器。
3. Paging3的实现
3.1 引入库
根据项目的需要添加库依赖到当前项目的Gradle文件中。
def paging_version = "3.0.0-beta01"
implementation "androidx.paging:paging-runtime:$paging_version"
// alternatively - without Android dependencies for tests
testImplementation "androidx.paging:paging-common:$paging_version"
// optional - RxJava2 support
implementation "androidx.paging:paging-rxjava2:$paging_version"
// optional - RxJava3 support
implementation "androidx.paging:paging-rxjava3:$paging_version"
// optional - Guava ListenableFuture support
implementation "androidx.paging:paging-guava:$paging_version"
// Jetpack Compose Integration
implementation "androidx.paging:paging-compose:1.0.0-alpha08"
3.2 配置DataStore
在Paging2中会有3种类型的Page Source
,这导致我们在使用之前需要好好考虑应该使用哪一种。这着实让开发者抓耳挠腮。
但是在Paging3种只有一种Page Source
,不用在为选择哪一种而苦恼了,直接闭着眼睛用就可以了。
Page Source
需要继承自PagingSource
, 同时需要重写load
和getRefreshKey
方法。
class PersonDataSource(private val repository: PersonRepository) : PagingSource<Int, Person>() {
// Load data when got data from DataSource(server or local cache).
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Person> {
val pos = params.key ?: START_INDEX
val startIndex = pos * params.loadSize + 1
val endIndex = (pos + 1) * params.loadSize
return try {
// Get data from DataSource
val personList = repository.getPersonList(startIndex, endIndex)
// Return data to RecyclerView by LoadResult
LoadResult.Page(
personList,
if (pos <= START_INDEX) null else pos - 1,
if (personList.isEmpty()) null else pos + 1
)
} catch (exception: Exception) {
// Return exception by LoadResult
LoadResult.Error(exception)
}
}
// Return the position of data when refresh RecyclerView.
override fun getRefreshKey(state: PagingState<Int, Person>): Int? {
return 0
}
companion object {
private const val START_INDEX = 0
}
}
从上面的代码可以知道,我们可以通过LoadParams
参数可以获得Paging的loadSize
和key
(起始位置)的信息。
3.3 创建一个可观察的数据集
接下来我们需要在ViewModel
中创建一个可观察的数据集。在这里所说的可观察数据集指的是LiveData
,Flow
和RxJava
中的Observable
和Flowable
等数据类型。需要值得注意的是,如果使用的RxJava
的数据集,不要忘记引入RxJava
的库。
在我的Demo中我使用的是Flow
。
var personList =
Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
initialLoadSize = 20
),
pagingSourceFactory = {
PersonDataSource(repository)
}
).flow
把PagingConfig
和PagingSource
传给Pager
, Pager
会返回一个Flow
。
在PagingConfig
中,我们需要设置PageSize
,enablePlaceholders
和initialLoadSize
。
在PagingSourceFactor
中传入我们在上面已经写好的DataSource。
3.4 创建一个Adapter
我们需要创建一个RecyclerView的adapter,该adapter需要继承自PagingDataAdapter
。具体的写法和ListAdapter
类似。
class PersonAdapter(private val context: Context) :
PagingDataAdapter<Person, PersonAdapter.ViewHolder>(PersonDiffCallback()) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val person = getItem(position)
holder.binding.also {
it.textViewName.text = person?.name
it.textViewAge.text = person?.age.toString()
if (position % 2 == 0) {
Glide.with(context).load(person?.photoUrl).into(it.imageView)
} else {
Glide.with(context).load(R.drawable.ic_studio_icon).into(it.imageView)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
RecyclerItemBinding.inflate(
LayoutInflater.from(context), parent, false
)
)
}
class ViewHolder(val binding: RecyclerItemBinding) :
RecyclerView.ViewHolder(binding.root)
}
class PersonDiffCallback : DiffUtil.ItemCallback<Person>() {
override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
return oldItem == newItem
}
}
3.5 显示item
这里的实现方法跟一般的RecyclerView中显示Item是一样的。简而言之,需要实现以下几项。
- 创建和设置adapter的实例。
- 创建coroutines.
- 在coroutines scope中接收Flow数据。
val adapter = PersonAdapter(this)
binding.recyclerView.adapter = adapter
// launch an coroutines, and received flow data. Finally, submit data to the adapter.
lifecycleScope.launch {
viewModel.personList.collectLatest {
adapter.submitData(it)
}
}
3.6 观察加载数据的状态
我们可以在adapter中设置一个listener来观察加载数据的状态。一共有3种状态。
LoadState.Loading
: 应用正在加载数据。LoadState.NotLoading
: 应用没有在加载数据。LoadState.Error
: 应用在加载数据的时候发生了错误。
adapter.addLoadStateListener {state ->
when(state.refresh){
is LoadState.Loading -> {
binding.swipeRefreshLayout.isRefreshing = true
}
is LoadState.NotLoading -> {
binding.swipeRefreshLayout.isRefreshing = false
}
is LoadState.Error -> {
// show an error dialog.
}
}
}
3.7 设置Header和Footer
我们可以在数据加载中和加载更多时显示特定的View。
首先,我们要创建一个继承自LoadStateAdapter
的Adapter,
具体实现方法跟PagingDataAdapter
的实现方法相似,不同的点在onBindViewHolder
方法上。
关于具体的实现可以参考下面代码。
class PersonLoadStateAdapter(private val context: Context) :
LoadStateAdapter<PersonLoadStateAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
// change the view when LoadStat was changed.
when(loadState){
is LoadState.Loading -> {
holder.binding.progressBar.visibility = View.VISIBLE
holder.binding.errorMsg.visibility = View.INVISIBLE
holder.binding.retryButton.visibility = View.INVISIBLE
}
is LoadState.NotLoading -> {
holder.binding.progressBar.visibility = View.INVISIBLE
holder.binding.errorMsg.visibility = View.INVISIBLE
holder.binding.retryButton.visibility = View.INVISIBLE
}
is LoadState.Error -> {
holder.binding.progressBar.visibility = View.INVISIBLE
holder.binding.errorMsg.visibility = View.VISIBLE
holder.binding.retryButton.visibility = View.VISIBLE
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
return ViewHolder(
LoadStateFooterViewItemBinding.inflate(
LayoutInflater.from(context), parent, false
)
)
}
class ViewHolder(val binding: LoadStateFooterViewItemBinding) :
RecyclerView.ViewHolder(binding.root)
}
第二步,我们可以通过withLoadStateHeaderAndFooter
方法把LoadStateAdapter
拼接到PagingDataAdapter
上。然后把拼接完后的Adapter传递给RecyclerView即可。
val adapter = PersonAdapter(this)
val concatAdapter: ConcatAdapter = adapter.withLoadStateFooter(
footer = PersonLoadStateAdapter(this)
)
binding.recyclerView.adapter = concatAdapter
4. Others
Github: github.com/HyejeanMOON…
让你易上手的Jetpack Compose教程
Jetpack相关教程
1. 让你易上手的Jetpack Paging3教程
2. 让你易上手的Jetpack DataStore教程
3. Android Jetpack Room的详细教程
4. Paging在Android中的应用
5. Android WorkManager的使用
其他教程
1. 神一样的存在,Dagger Hilt !!
2. Android10的分区存储机制(Scoped Storage)适配教程
3. Android的属性动画(Property Animation)详细教程
4. Android ConstraintLayout的易懂教程
5. Google的MergeAdapter的使用