分页(Paging)库概览

原文链接:Paging library overview | Android Developers

分页库可帮助您一次加载和显示少量数据。 按需加载部分数据可减少网络带宽和系统资源的使用。

本指南提供了库的几个概念性示例,以讲解它如何工作。 要查看此库如何运行的完整示例,请尝试使用 codelab 或者从 其他资源 中获取示例。

分页库架构

本节介绍并展示分页库的主要组件。

  • PagedList

分页库的关键组件是 PagedList 类,它分块加载应用程序的数据或页面。当需要更多数据时,会被加载到已有的 PagedList 对象当中。 如果任何加载的数据发生更改,则会从 LiveData 或基于 RxJava2 的对象向可观察数据持有者发射新的 PagedList 实例。PagedList 对象一旦生成,应用的 UI 就会展示内容,同时遵循 UI 控制器的 生命周期

以下代码片段展示了如何使用 PagedList 对象的 LiveData 持有者来配置应用程序的视图模型(view model),以便加载和显示数据:

  1. public class ConcertViewModel extends ViewModel {
  2. private ConcertDao concertDao;
  3. public final LiveData<PagedList<Concert>> concertList;
  4. // 创建一个每页 50 条记录的 PageList 对象
  5. public ConcertViewModel(ConcertDao concertDao) {
  6. this.concertDao = concertDao;
  7. concertList = new LivePagedListBuilder<>(
  8. concertDao.concertsByDate(), 50).build();
  9. }
  10. }
  • 数据

每个 PagedList 实例都从相应的 DataSource 对象中加载应用数据的最新快照。数据从应用的后端服务器或数据库流向 PagedList 对象。

以下示例使用 Room 数据持久化库 来组织应用程序的数据,但如果您想使用其他方法存储数据,还可以提供自己的 数据源工厂

  1. @Dao
  2. public interface ConcertDao {
  3. // 整型参数告诉 Room 应当使用
  4. // PositionalDataSource 对象
  5. @Query("SELECT * FROM concerts ORDER BY date DESC")
  6. DataSource.Factory<Integer, Concert> concertsByDate();
  7. }

欲了解更多有关加载数据到 PagedList 对象的信息,请参阅数据组件和注意事项

  • UI

PagedList 使用 PagedListAdapter 把数据条目(item)加载到 RecyclerView 中。这些类齐心协力地加载数据和展示结果,以及预读取视图内容,并负责绘制内容变化时的动画。

欲了解更多信息,请参阅界面组件和注意事项

支持不同的数据架构

分页库支持下列数据架构:

  • 仅从后端网络服务器提供。
  • 仅存储在设备上的数据库中。
  • 使用设备上数据库作为缓存并与其他数据源组合的形式。

图1显示了每种架构方案中数据的流动方式。 对于仅限网络或仅限数据库的解决方案,数据直接流向应用程序的 UI 模型(model)。 如果您使用的是组合方法,则数据会先从后端服务器流入设备上的数据库,再流入应用程序的 UI 模型。 每隔一段时间,每个数据流末端就会耗尽要加载的数据,此时它会从提供数据的组件中请求更多数据。 例如,当用完设备上数据库的数据后,它会从服务器请求更多数据。

paging-library-data-flow

图1 数据如何流经分页库支持的每个体系结构

只有网络

欲展示从后端服务器获得的数据,请使用同步版本的 Retrofit API 来把信息加载到 您自定义的 DataSource 对象 中。

注意:分页库的 DataSource 对象并不提供任何错误处理的功能,因为不同的应用处理和表示错误的方法都不一样。如果发生了错误,请延迟处理结果回调,并过一会儿重试。有关此行为的示例,请参阅 PagingWithNetwork sample

只有数据库

设置 RecyclerView 观测本地存储,最好使用 Room 数据持久化库。这样的话,应用程序的数据库无论何时新增或修改了数据,这些变动都会自动反映到正在显示该数据的 RecyclerView 中。

网络和数据库都有

当您开始观测数据库后,您可以使用 PagedList.BoundaryCallback 来监听数据库的数据是否已经全部加载完毕。您可以从网络获取更多的条目(item)并将其插入数据库。如果您的 UI 正在观测数据库,那么您就无须烦劳操心了。

处理网络错误

在使用分页库来从网络加载数据或者将其分页显示时,请务必不要总是把网络视作“可用”和“不可用”的二元状态,因为许多时候网络连接是时断时续或不稳定的:

  • 某些服务器也许没能响应网络请求。
  • 设备可能连接到了一个网速缓慢或者信号弱的网络。

相反,您的应用应检查每个失败请求,并在网络不可用的情况下尽可能优雅地恢复。例如,您可以提供“重试”按钮,在数据刷新的步骤不起作用时供用户手动选择。如果数据分页的步骤出现了错误,最好自动重试分页请求。

更新您已有的应用

如果您的应用已经从数据库或者后端服务器的数据源来获取数据的话,您可以直接将其升级到分页库提供的功能。本小节展示了如何升级一个已有常见设计模式的应用。

自定义的分页解决方案

如果您使用自定义的功能来从应用的数据源加载少量的数据子集,那么您可以把它们替换成 PagedList 类中的相应逻辑。PagedList 实例内建了连接到常见数据源的功能,而且还提供了您的 UI 中可能使用的 RecyclerView 的适配器。

使用列表而非分页加载数据

如果您使用内存列表来作为 UI 适配器的支持数据结构,那么在列表可能变得很大的情况下,请考虑使用 PagedList 来观察数据的更新。PagedList 实例可以使用 LiveData<PagedList> 或者 Observable<List> 来把数据更新传递到 UI,尽可能减少了数据加载时间和内存消耗。不过,尽量还是把 List 对象替换成 PagedList 比较好,因为这样无须劳烦您更改现有的应用 UI 结构或者数据更新逻辑。

使用 CursorAdapter 将数据游标(cursor)与列表视图相关联

您的应用可以使用 CursorAdapter 来把 Cursor 的数据关联到 ListView 中。在这种情况下,您通常需要把应用中的列表 UI 容器从 ListView 迁移到 RecyclerView,然后根据 Cursor 是否访问的是 SQLite 数据库,来决定是把其组件替换成 Room 还是 PositionalDataSource

在某些情况下,例如使用 Spinner 的时候,您只需提供适配器自身,某个库就会接着处理该适配器中加载的数据并展示给您。这时,把您适配器的数据的类型改成 LiveData<PagedList>,然后将其包装到一个 ArrayAdapter 对象里,最后再使用库将这些数据填充到 UI 中。

使用 AsyncListUtil 异步加载内容

如果您使用 AsyncListUtil 来异步加载并展示分组的信息,分页库能让您的工作更轻松:

  • 您的数据无须是按顺序排列的。 分页库让您使用键(key)直接从后端服务器加载数据。
  • 您的数据量可以特别巨大。 使用分页库,您可以不停加载数据到分页中,直到耗尽数据源。
  • 您可以更轻易地观察数据。 分页库能展示您应用的 ViewModel 中储存的可观察的数据结构。

注意:如果您的应用访问的是 SQLite 数据库,请参阅 Room 数据持久化库

数据库示例

以下代码片段展示了一些可能的整合所有模块的方法。

使用 LiveData 来观察分页的数据

如下所示,当数据库中的演唱会事件(concert event)被添加、移除或者修改时,RecyclerView 中的内容会被自动而高效地更新:

  1. @Dao
  2. public interface ConcertDao {
  3. // 整型参数告诉 Room 应当使用 PositionalDataSource 对象,
  4. // 并由 Room 加载基于位置的数据。
  5. @Query("SELECT * FROM concerts ORDER BY date DESC")
  6. DataSource.Factory<Integer, Concert> concertsByDate();
  7. }
  8. public class ConcertViewModel extends ViewModel {
  9. private ConcertDao mConcertDao;
  10. public final LiveData<PagedList<Concert>> concertList;
  11. public ConcertViewModel(ConcertDao concertDao) {
  12. mConcertDao = concertDao;
  13. concertList = new LivePagedListBuilder<>(
  14. mConcertDao.concertsByDate(), /* 分页大小 */ 20).build();
  15. }
  16. }
  17. public class ConcertActivity extends AppCompatActivity {
  18. @Override
  19. public void onCreate(@Nullable Bundle savedInstanceState) {
  20. super.onCreate(savedInstanceState);
  21. ConcertViewModel viewModel =
  22. ViewModelProviders.of(this).get(ConcertViewModel.class);
  23. RecyclerView recyclerView = findViewById(R.id.concert_list);
  24. ConcertAdapter adapter = new ConcertAdapter();
  25. viewModel.concertList.observe(this, adapter::submitList);
  26. recyclerView.setAdapter(adapter);
  27. }
  28. }
  29. public class ConcertAdapter
  30. extends PagedListAdapter<Concert, ConcertViewHolder> {
  31. protected ConcertAdapter() {
  32. super(DIFF_CALLBACK);
  33. }
  34. @Override
  35. public void onBindViewHolder(@NonNull ConcertViewHolder holder,
  36. int position) {
  37. Concert concert = getItem(position);
  38. if (concert != null) {
  39. holder.bindTo(concert);
  40. } else {
  41. // Null 定义了一个占位符条目(placeholder item)- 当真正的数据项
  42. // 从数据库中加载出来时,PagedListAdapter 会自动把这行作废。
  43. holder.clear();
  44. }
  45. }
  46. private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
  47. new DiffUtil.ItemCallback<Concert>() {
  48. // 从数据库中重新加载演唱会数据后,其细节可能已经发生了变化,
  49. // 然而其 ID 仍然是固定不变的。
  50. @Override
  51. public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
  52. return oldConcert.getId() == newConcert.getId();
  53. }
  54. @Override
  55. public boolean areContentsTheSame(Concert oldConcert,
  56. Concert newConcert) {
  57. return oldConcert.equals(newConcert);
  58. }
  59. };
  60. }

使用 RxJava2 观察分页的数据

如果您更愿意使用 RxJava2 而不是 LiveData,您可以创建一个 Observable 或者 Flowable 对象:

  1. public class ConcertViewModel extends ViewModel {
  2. private ConcertDao mConcertDao;
  3. public final Flowable<PagedList<Concert>> concertList;
  4. public ConcertViewModel(ConcertDao concertDao) {
  5. mConcertDao = concertDao;
  6. concertList = new RxPagedListBuilder<>(
  7. mConcertDao.concertsByDate(), /* 分页大小 */ 50)
  8. .buildFlowable(BackpressureStrategy.LATEST);
  9. }
  10. }

您接下来就可以使用如下代码开始或停止观察数据:

  1. public class ConcertActivity extends AppCompatActivity {
  2. private ConcertAdapter mAdapter;
  3. private ConcertViewModel mViewModel;
  4. private CompositeDisposable mDisposable = new CompositeDisposable();
  5. @Override
  6. public void onCreate(@Nullable Bundle savedInstanceState) {
  7. super.onCreate(savedInstanceState);
  8. RecyclerView recyclerView = findViewById(R.id.concert_list);
  9. mViewModel = ViewModelProviders.of(this).get(ConcertViewModel.class);
  10. mAdapter = new ConcertAdapter();
  11. recyclerView.setAdapter(mAdapter);
  12. }
  13. @Override
  14. protected void onStart() {
  15. super.onStart();
  16. mDisposable.add(mViewModel.concertList.subscribe(
  17. flowableList -> mAdapter.submitList(flowableList)
  18. ));
  19. }
  20. @Override
  21. protected void onStop() {
  22. super.onStop();
  23. mDisposable.clear();
  24. }
  25. }

ConcertDaoConcertAdapter 的代码在 RxJava2LiveData 这两种方案中是一致的。

提交反馈

通过以下途径分享您的反馈和想法:

Issue tracker bug

报告问题以便我们修复。

其他资源

欲了解更多有关分页库的信息,请参阅以下资源:

示例

Codelabs

视频