分页库
文章目录
分页库可帮助您一次加载和显示一小块数据。按需载入部分数据会减少网络带宽和系统资源的使用量。
声明依赖项
dependencies {
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 Kotlin use paging-rxjava2-ktx
}
概览
库架构
PageList
分页库的关键组件是 PagedList 类,用于加载应用数据块或页面。随着所需数据的增多,系统会将其分页到现有的 PagedList 对象中。如果任何已加载的数据发生更改,会从 LiveData 或基于 RxJava2 的对象向可观察数据存储器发出一个新的 PagedList 实例。随着 PagedList 对象的生成,应用界面会呈现其内容,同时还会考虑界面控件的生命周期。
示例
以下代码段展示了如何配置应用的视图模型,以便使用 PagedList 对象的 LiveData 存储器加载和显示数据:
class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
val concertList: LiveData<PagedList<Concert>> =
concertDao.concertsByDate().toLiveData(pageSize = 50)
}
数据
每个 PagedList 实例都会从对应的 DataSource 对象加载应用数据的最新快照。数据从您应用的后端或数据库流向 PagedList 对象。
以下示例使用 Room 持久性库来整理应用数据,但如果要通过其他方式存储数据,也可以提供自己的数据源工厂:
@Dao
interface ConcertDao {
// The Int type parameter tells Room to use a PositionalDataSource object.
@Query("SELECT * FROM concerts ORDER BY date DESC")
fun concertsByDate(): DataSource.Factory<Int, Concert>
}
界面
PagedList 类使用 PagedListAdapter 将项加载到 RecyclerView。这些类共同作用,在内容加载时抓取和显示内容,预取不在视线范围内的内容以及针对内容更改添加动画。
支持不同的数据架构
网络
要显示来自后端服务器的数据,请使用同步版本的 Retrofit API,将信息加载到您自己的自定义 DataSource
对象中。
注意:由于不同的应用处理和显示错误界面的方式不同,因此分页库的 DataSource
对象不提供任何错误处理。如果发生错误,请遵循结果回调,并在稍后重试请求。有关此行为的示例,请参阅 PagingWithNetwork 示例。
数据库
设置您的 RecyclerView
以观察本地存储空间,最好使用 Room 持久性库。这样,无论您何时在应用数据库中插入或修改数据,这些更改都会自动反映在显示此数据的 RecyclerView
中。
网络和数据库
在开始观察数据库之后,您可以使用 PagedList.BoundaryCallback
监听数据库中的数据何时耗尽。然后,您可以从网络中获取更多项目并将它们插入到数据库中。如果界面正在观察数据库,则您只需执行此操作即可
处理网络错误
通过分页库,使用网络对要显示的数据进行抓取或分页时,请务必不要始终将网络视为“可用”或“不可用”,因为许多连接会断断续续或不稳定:
- 特定服务器可能无法响应网络请求。
- 设备可能连接到速度较慢或信号较弱的网络。
您的应用应检查每个请求是否失败,并在网络不可用的情况下尽可能正常恢复。例如,如果数据刷新步骤不起作用,您可以提供“重试”按钮供用户选择。如果在数据分页步骤中发生错误,则最好自动重新尝试分页请求。
更新现有应用
自定义分页解析
如果您使用自定义功能从应用的数据源加载较小的数据子集,则可以将此逻辑替换为 PagedList 类中的逻辑。PagedList 实例提供了与常见数据源的内置连接。这些实例还为应用界面中可能包含的 RecyclerView 对象提供了适配器。
使用列表而不是网页加载的数据
如果您使用内存中列表作为界面适配器的后备数据结构,并且列表中的项目数量可能会变得非常大,请考虑使用 PagedList
类观察数据更新。
__PagedList 实例可以使用 LiveData
或 Observable
向您的应用界面传递数据更新,从而最大限度地缩短加载时间并减少内存用量。__在应用中将 List
对象替换成 PagedList
对象会得到更理想的结果,因为后者不需要对应用界面结构或数据更新逻辑进行任何更改。
使用CursorAdapter将数据光标与列表视图相关联
您的应用可能会使用 CursorAdapter 将 Cursor 的数据与 ListView 相关联。
在这种情况下,您通常需要:
-
从 ListView 迁移到 RecyclerView,以后者作为应用的列表界面容器,
-
将 Cursor 组件替换为 Room 或 PositionalDataSource,具体取决于 Cursor 实例是否会访问 SQLite 数据库。
在某些情况下,例如在使用 Spinner 的实例时,您只需提供适配器本身。然后,库将获取加载到该适配器中的数据,并为您显示这些数据。
在这类情况下,请将适配器的数据类型更改为 LiveData,然后将此列表封装到 ArrayAdapter 对象中,再尝试让库类扩充界面中的这些项目。
使用AsyncListUtil异步加载内容
如果您使用 AsyncListUtil
对象来异步加载和显示信息组,则通过分页库可以更轻松地加载数据:
- **您的数据无需固定位置。**通过分页库,您可以使用网络提供的密钥直接从后端加载数据。
- **您的数据可能会非常庞大。**通过分页库,您可以将数据加载到网页中,直到没有剩余数据为止。
- **您可以更轻松地观察数据。**分页库可以为您呈现应用 ViewModel 存储在可观察数据结构中的数据。
数据库示例
使用LiveData观察分页数据
以下代码段显示了完整代码。随着在数据库中添加、移除或更改 concert 事件,RecyclerView
中的内容会自动且高效地更新:
@Dao
interface ConcertDao {
// The Int type parameter tells Room to use a PositionalDataSource
// object, with position-based loading under the hood.
@Query("SELECT * FROM concerts ORDER BY date DESC")
fun concertsByDate(): DataSource.Factory<Int, Concert>
}
class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
val concertList: LiveData<PagedList<Concert>> =
concertDao.concertsByDate().toLiveData(pageSize = 50)
}
class ConcertActivity : AppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = ViewModelProviders.of(this)
.get<ConcertViewModel>()
val recyclerView = findViewById(R.id.concert_list)
val adapter = ConcertAdapter()
viewModel.livePagedList.observe(this, PagedList(adapter::submitList))
recyclerView.setAdapter(adapter)
}
}
class ConcertAdapter() :
PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
val concert: Concert? = getItem(position)
// Note that "concert" is a placeholder if it's null.
holder.bindTo(concert)
}
companion object {
private val DIFF_CALLBACK = object :
DiffUtil.ItemCallback<Concert>() {
// Concert details may have changed if reloaded from the database,
// but ID is fixed.
override fun areItemsTheSame(oldConcert: Concert,
newConcert: Concert) = oldConcert.id == newConcert.id
override fun areContentsTheSame(oldConcert: Concert,
newConcert: Concert) = oldConcert == newConcert
}
}
}
使用RxJava2观察分页数据
如果您倾向于使用 RxJava2 而不是 LiveData
,则可以改为创建 Observable
或 Flowable
对象:
class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
val concertList: Observable<PagedList<Concert>> =
concertDao.concertsByDate().toObservable(pageSize = 50)
}
然后,您可以使用以下代码段中的代码来开始和停止观察数据:
class ConcertActivity : AppCompatActivity() {
private val adapter: ConcertAdapter()
private lateinit var viewModel: ConcertViewModel
private val disposable = CompositeDisposable()
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val recyclerView = findViewById(R.id.concert_list)
viewModel = ViewModelProviders.of(this)
.get<ConcertViewModel>()
recyclerView.setAdapter(adapter)
}
override fun onStart() {
super.onStart()
disposable.add(viewModel.concertList
.subscribe(adapter::submitList)))
}
override fun onStop() {
super.onStop()
disposable.clear()
}
}
对于基于 RxJava2 的解决方案,
ConcertDao
和ConcertAdapter
的代码是相同的,对于基于LiveData
的解决方案也是如此。
显示分页列表
本指南基于_分页库概览_,介绍了如何在应用界面中向用户显示信息列表,尤其是在此信息发生变化时。
将界面与视图模型关联
将 LiveData
的实例连接到 PagedListAdapter
,如以下代码段所示:
private val adapter = ConcertAdapter()
private lateinit var viewModel: ConcertViewModel
override fun onCreate(savedInstanceState: Bundle?) {
viewModel = ViewModelProviders.of(this).get(ConcertViewModel::class.java)
viewModel.concerts.observe(this, Observer { adapter.submitList(it) })
}
当数据源提供 PagedList 的新实例时,Activity 会将这些对象发送到适配器。PagedListAdapter 实现定义了更新的计算方式,并自动处理分页和列表差异。因此,您的 ViewHolder 只需要绑定到提供的特定项即可:
class ConcertAdapter() :
PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
override fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
val concert: Concert? = getItem(position)
// Note that "concert" is a placeholder if it's null.
holder.bindTo(concert)
}
companion object {
private val DIFF_CALLBACK = ... // See Implement the diffing callback section.
}
}
PagedListAdapter
使用 PagedList.Callback
对象处理网页加载事件。当用户滚动时,PagedListAdapter
会调用 PagedList.loadAround()
来向底层 PagedList
提供关于应从 DataSource
获取哪些项的提示。
注意:PagedList
具有内容不可变特性。这意味着即使可以将新内容加载到 PagedList
的实例中,但加载的项目本身不会在加载后立即改变。因此,如果 PagedList
中的内容更新,则 PagedListAdapter
对象会收到包含更新后信息的全新 PagedList
。
实现差异回调
以下示例展示了用于比较相关对象字段的 areContentsTheSame()
的手动实现:
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Concert>() {
// The ID property identifies when items are the same.
override fun areItemsTheSame(oldItem: Concert, newItem: Concert) =
oldItem.id == newItem.id
// If you use the "==" operator, make sure that the object implements
// .equals(). Alternatively, write custom data comparison logic here.
override fun areContentsTheSame(
oldItem: Concert, newItem: Concert) = oldItem == newItem
}
由于适配器包含比较项的定义,因此适配器会在有新的
PagedList
对象加载时自动检测这些项的更改。这样,适配器就会在您的RecyclerView
对象内触发有效的项目动画。
使用其它适配器类型实现差异回调功能
如果您选择不从 PagedListAdapter
继承(例如,当您使用的库提供自己的适配器时),仍可以直接通过 AsyncPagedListDiffer
对象来使用分页库适配器的差异化功能。
在界面中提供占位符(placeholder)
如果您希望界面在应用完成数据获取前显示列表,可以向用户显示占位符列表项。PagedList
对这种情况的处理方式是将列表项数据显示为 null
,直到加载了数据为止。
注意:默认情况下,分页库支持这种占位符行为。
占位符具有以下优点:
- 支持滚动条:
PagedList
可向PagedListAdapter
提供列表项数量。此信息允许适配器绘制滚动条来传达整个列表大小。有新页面载入时,滚动条不会跳到指定位置,因为列表不会改变大小。 - 无需加载旋转图标:由于列表大小已知,因此无需提醒用户正在加载更多项。占位符本身会传达这一信息。
不过,在添加对占位符的支持之前,请注意以下前提条件:
- 需要可计数的数据集:Room 持久性库 中的
DataSource
实例可以有效地计算项的数量。但如果您使用的是自定义本地存储解决方案或网络专用数据架构,确定数据集包含多少项可能会开销极大,甚至根本无法确定。 - 适配器必须考虑未加载的项:为准备列表以应对增长而使用的适配器或呈现机制需要处理 Null 列表项。例如,将数据绑定到
ViewHolder
时,您需要提供默认值来表示未加载数据。 - 需要同样大小的项视图:如果列表项大小会随着内容而变(例如社交网络更新),则项之间的交叉渐变效果并不理想。在这种情况下,我们强烈建议停用占位符。
加载分页数据
讨论如何自定义应用的数据加载解决方案以满足应用的架构需求
构造可观察列表
通常,您的界面代码会观察
LiveData
对象(如果您使用 RxJava2,则会观察Flowable
或Observable
对象),该对象位于您应用的ViewModel
中。此可观察对象搭起应用列表数据的呈现与内容之间的关联。为了创建其中一个可观察的
PagedList
对象,请将DataSource.Factory
的实例传递到LivePagedListBuilder
或RxPagedListBuilder
对象。DataSource
对象会加载单个PagedList
的页面。Factory 类会创建新的PagedList
实例来响应内容更新,例如数据库表失效和网络刷新。Room 持久性库可为您提供DataSource.Factory
对象,您也可以构建自己的对象。
以下代码段展示了如何使用 Room 的 DataSource.Factory
构建功能在应用的 ViewModel
类中创建新的 LiveData
实例:
ConcertDao
@Dao
interface ConcertDao {
// The Int type parameter tells Room to use a PositionalDataSource
// object, with position-based loading under the hood.
@Query("SELECT * FROM concerts ORDER BY date DESC")
fun concertsByDate(): DataSource.Factory<Int, Concert>
}
ConcertViewModel
// The Int type argument corresponds to a PositionalDataSource object.
val myConcertDataSource : DataSource.Factory<Int, Concert> =
concertDao.concertsByDate()
val concertList = myConcertDataSource.toLiveData(pageSize = 50)
自定义分页配置
要进一步为高级用例配置
LiveData
,您还可以定义自己的分页配置。特别是,您可以定义以下特性:
如果您希望更好地控制分页库何时从应用数据库加载列表,请将自定义 Executor
对象传递给 LivePagedListBuilder
,如以下代码段所示:
ConcertViewModel
val myPagingConfig = Config(
pageSize = 50,
prefetchDistance = 150,
enablePlaceholders = true
)
// The Int type argument corresponds to a PositionalDataSource object.
val myConcertDataSource : DataSource.Factory<Int, Concert> =
concertDao.concertsByDate()
val concertList = myConcertDataSource.toLiveData(
pagingConfig = myPagingConfig,
fetchExecutor = myExecutor
)
选择合适的数据源类型
请务必连接到能最好地处理源数据结构的数据源:
如果您加载的网页嵌入了上一页/下一页的键,请使用
PageKeyedDataSource
。例如,如果您从网络中获取社交媒体帖子,则可能需要将一个nextPage
令牌从一次加载传递到后续加载。如果您需要使用项目 N 中的数据来获取项目 N+1,请使用
ItemKeyedDataSource
。例如,如果您要为讨论应用获取会话式评论,则可能需要传递最后一条评论的 ID 以获取下一条评论的内容。如果您需要从数据存储区中选择的任意位置获取数据页,请使用
PositionalDataSource
。该类支持从您选择的任意位置开始请求一组数据项。例如,该请求可能会返回从位置 1500 开始的 50 个数据项。
数据无效时发送通知
当使用分页库时,由数据层在表或行已过时通知应用的其他层。为此,请从您为应用选择的 DataSource
类中调用 invalidate()
。
注意:应用界面可以使用下拉刷新模型触发此数据失效功能。
自定义数据源
如果您使用自定义本地数据解决方案,或直接从网络加载数据,则可以实现其中一个 DataSource
子类。
以下代码段展示了从指定音乐会开始时间开始的数据源:
class ConcertTimeDataSource() :
ItemKeyedDataSource<Date, Concert>() {
override fun getKey(item: Concert) = item.startTime
override fun loadInitial(
params: LoadInitialParams<Date>,
callback: LoadInitialCallback<Concert>) {
val items = fetchItems(params.requestedInitialKey,
params.requestedLoadSize)
callback.onResult(items)
}
override fun loadAfter(
params: LoadParams<Date>,
callback: LoadCallback<Concert>) {
val items = fetchItemsAfter(
date = params.key,
limit = params.requestedLoadSize)
callback.onResult(items)
}
}
然后,您可以通过创建具体的 DataSource.Factory
子类,将此自定义数据加载到 PagedList
对象中。
以下代码段展示了如何生成前面代码段中定义的自定义数据源的新实例:
class ConcertTimeDataSourceFactory :
DataSource.Factory<Date, Concert>() {
val sourceLiveData = MutableLiveData<ConcertTimeDataSource>()
var latestSource: ConcertDataSource?
override fun create(): DataSource<Date, Concert> {
latestSource = ConcertTimeDataSource()
sourceLiveData.postValue(latestSource)
return latestSource
}
}
考虑内容更新的运作方式
构建可观察的 PagedList
对象时,请考虑内容更新的运作方式。如果直接从 Room 数据库加载数据,则更新会自动推送至应用界面。
使用分页网络 API 时,您通常需要使用“下拉刷新”这样的用户互动,以指示系统让最近使用的 DataSource
的失效。然后请求该数据源的新实例。
以下代码段演示了此行为:
class ConcertActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// ...
concertTimeViewModel.refreshState.observe(this, Observer {
// Shows one possible way of triggering a refresh operation.
swipeRefreshLayout.isRefreshing =
it == MyNetworkState.LOADING
})
swipeRefreshLayout.setOnRefreshListener {
concertTimeViewModel.invalidateDataSource()
}
}
}
class ConcertTimeViewModel(firstConcertStartTime: Date) : ViewModel() {
val dataSourceFactory = ConcertTimeDataSourceFactory(firstConcertStartTime)
val concertList: LiveData<PagedList<Concert>> =
dataSourceFactory.toLiveData(
pageSize = 50,
fetchExecutor = myExecutor
)
fun invalidateDataSource() =
dataSourceFactory.sourceLiveData.value?.invalidate()
}
提供数据映射
分页库支持基于项目或基于页面转换由 DataSource
加载的项目。
在以下代码段中,音乐会名称和音乐会日期的组合映射到同时包含名称和日期的单个字符串:
class ConcertViewModel : ViewModel() {
val concertDescriptions : LiveData<PagedList<String>> {
init {
val concerts = database.allConcertsFactory()
.map "${it.name} - ${it.date}" }
.toLiveData(pageSize = 50)
}
}
}
如果您希望在项目加载后进行换行、转换或准备,这将非常有用。由于这项工作是在提取执行程序上完成的,因此您可以执行开销可能很大的工作,如从磁盘读取或查询单独的数据库。
注意:JOIN 查询作为
map()
的一部分,总是能够更高效地进行重新查询。