Paging 库可以帮助我们加载显示一小部分数据。按需加载部分数据可以减少网络带宽和系统资源的占用。
本指南提供了库的几个概念性示例和它们是如何工作的概述。
在项目中引入 Paging,需要添加以下依赖:
dependencies {
def paging_version = "2.1.0"
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
}
本节主要描述 Paging 库的主要组成部分。
Paging 库的关键就是 PagedList 类,它会加载页面或者应用数据的一部分。由于需要很多的数据,因为将它们分页到现有的 PagedList 对象当中。如果任何加载的数据发生更改,则会从 基于 LiveData 或 RxJava2 的对象向可观察数据持有者发出新的 PagedList 实例。生成新的 PagedList 对象时,应用程序的 UI 会显示其内容,同时遵守 UI 控制器的生命周期。
下面的代码显示了如何使用 PagedList 对象的 LiveData 持有者配置应用程序的视图模型以加载和显示数据:
public class ConcertViewModel extends ViewModel {
private ConcertDao concertDao;
public final LiveData<PagedList<Concert>> concertList;
// Creates a PagedList object with 50 items per page.
public ConcertViewModel(ConcertDao concertDao) {
this.concertDao = concertDao;
concertList = new LivePagedListBuilder<>(
concertDao.concertsByDate(), 50).build();
}
}
PagedList 的每个实例都从其相应的 DataSource 对象加载应用程序数据的最新快照。数据从应用程序的后端或数据库流入 PagedList 对象。
以下示例使用 Room 库来组织应用程序的数据,但如果要使用其他方法存储数据,还可以提供自己的数据源工厂。
@Dao
public interface ConcertDao {
// The Integer type parameter tells Room to use a
// PositionalDataSource object.
@Query("SELECT * FROM concerts ORDER BY date DESC")
DataSource.Factory<Integer, Concert> concertsByDate();
}
要了解有关如何将数据加载到 PagedList 对象的更多信息,请参阅有关如何加载分页数据的指南。
在 RecyclerView 中设置 PagedListAdapter 和 PagedList类 ,这些类一起工作以在加载内容时获取和显示内容,预取视图内容并动画内容更改。
Paging 库支持从以下数据结构:
上图显示了每种架构中数据的流动方式。对于仅限网络或者仅限数据库的方案,数据直接流向程序的 UI 模型。如果您使用的是组合方式,则数据会从后端服务器流向设备上的数据库,然后流入程序的 UI 模型。每隔一段时间,每个数据流的端点就会耗尽要加载的数据,此时它会从提供数据的组件请求更多的数据,例如当设备上的数据库用完数据时,它会从服务器请求更多的数据。
要显示来自服务器的数据,请使用 Retrofit API 的同步版本将信息加载到你自己定义的 DataSource 对象中。
注意,Paging 库的 DataSource 对象不提供任何错误处理,因为不同的程序使用不同的方式处理和显示错误 UI。如果发生错误,请遵循结果回调,稍候重试该请求。有关这样的例子,请参阅 PagingWithNetwork 示例
我们需要设置 RecyclerView 观察本地存储,最好使用 Room数据库,这样,无论何时在程序的数据库中插入或修改数据,这些更改都会自动反映在显示此数据的 RecyclerView
中。
在开始观察数据库后,可以使用 PagedList.BoundaryCallback 监听实时监控什么时候没有数据。然后就可以从网络中获取更多并将其插入到数据库中。
当我们使用网络获取或分页 Paging Library 显示的数据时,不要一直将网络视为“可用”或“不可用”,因为许多连接是断断续续性的或片段状的:
相反,我们的应用应检查每个失败请求,并在网络不可用的情况下尽可能地恢复。例如,我们可以提供“重试”按钮,在数据刷新步骤不起作用时,可以供用户选择。如果在数据分页步骤期间发生错误,则最好自动重试分页请求。
如果 APP 从数据库或后端中获取数据,可以直接升级到 Paging 库提供的功能。本节介绍如何升级现有设计的应用程序。
如果你使用自定义功能从App的数据源分页加载数据,就可以将此逻辑替换为 PagedList 类中的逻辑 。PagedList
的实例提供与数据源的内置连接 ,PagedList
的实例还为 UI 中的 RecyclerView 提供适配器。
如果你为 UI 适配器 使用处于内存中的列表作为后备数据结构,如果列表中的条目数可能增大,可以考虑使用 PagedList 类来观察数据更新。PagedList 的实例可以使用 LiveData< PagedList>
或 Observable<List>
将数据更新传递到应用程序的UI,从而最大限度地减少加载时间和内存使用量。而且使用 PagedList 对象替换应用程序中的 List 的不需要对应用程序的 UI 结构或数据更新逻辑进行任何更改。
如果你的应用程序使用 CursorAdapter 将数据从一个 Cursor 关联到 ListView。在这种情况下,你通常需要从 ListView 迁移到 RecyclerView 作为应用程序的列表UI容器,然后将该 Cursor 组件替换为 Room 或 PositionalDataSource,取决于 Cursor 实例是否访问 SQLite数据库。
在某些情况下,例如在处理 Spinner 实例时 ,你只需提供适配器本身,然后将获取到的数据加载到该适配器中并显示。在这些情况下,将适配器数据类型更改为 LiveData< PagedList>
,然后将此列表封装在 ArrayAdapter 对象中。
如果我们使用 AyncListUtil 对象异步加载和显示信息,Paging Library 可以让我们更轻松地加载数据:
注意:如果你的应用程序需要访问SQLite数据库,可以查看 Android架构组件- Room数据库的使用。
以下代码片段显示了几种可能的协同工作方法。
以下代码段显示了一起工作的所有部分。 随着在数据库中添加,删除或更改 Concert 事件,RecyclerView 中的内容将自动且高效地更新:
@Dao
public interface ConcertDao {
// The Integer type parameter tells Room to use a PositionalDataSource
// object, with position-based loading under the hood.
@Query("SELECT * FROM concerts ORDER BY date DESC")
DataSource.Factory<Integer, Concert> concertsByDate();
}
public class ConcertViewModel extends ViewModel {
private ConcertDao concertDao;
public final LiveData<PagedList<Concert>> concertList;
public ConcertViewModel(ConcertDao concertDao) {
this.concertDao = concertDao;
concertList = new LivePagedListBuilder<>(
concertDao.concertsByDate(), /* page size */ 50).build();
}
}
public class ConcertActivity extends AppCompatActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ConcertViewModel viewModel =
ViewModelProviders.of(this).get(ConcertViewModel.class);
RecyclerView recyclerView = findViewById(R.id.concert_list);
ConcertAdapter adapter = new ConcertAdapter();
viewModel.concertList.observe(this, adapter::submitList);
recyclerView.setAdapter(adapter);
}
}
public class ConcertAdapter
extends PagedListAdapter<Concert, ConcertViewHolder> {
protected ConcertAdapter() {
super(DIFF_CALLBACK);
}
@Override
public void onBindViewHolder(@NonNull ConcertViewHolder holder,
int position) {
Concert concert = getItem(position);
if (concert != null) {
holder.bindTo(concert);
} else {
// Null defines a placeholder item - PagedListAdapter automatically
// invalidates this row when the actual object is loaded from the
// database.
holder.clear();
}
}
private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
new DiffUtil.ItemCallback<Concert>() {
// Concert details may have changed if reloaded from the database,
// but ID is fixed.
@Override
public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
return oldConcert.getId() == newConcert.getId();
}
@Override
public boolean areContentsTheSame(Concert oldConcert,
Concert newConcert) {
return oldConcert.equals(newConcert);
}
};
}
如果你更喜欢使用 RxJava2 而不是 LiveData,则可以创建一个 Observable
或Flowable
对象:
public class ConcertViewModel extends ViewModel {
private ConcertDao concertDao;
public final Observable<PagedList<Concert>> concertList;
public ConcertViewModel(ConcertDao concertDao) {
this.concertDao = concertDao;
concertList = new RxPagedListBuilder<>(
concertDao.concertsByDate(), /* page size */ 50)
.buildObservable();
}
}
然后你可以使用下面的代码开始和停止观察数据:
public class ConcertActivity extends AppCompatActivity {
private ConcertAdapter adapter = new ConcertAdapter();
private ConcertViewModel viewModel;
private CompositeDisposable disposable = new CompositeDisposable();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RecyclerView recyclerView = findViewById(R.id.concert_list);
viewModel = ViewModelProviders.of(this).get(ConcertViewModel.class);
recyclerView.setAdapter(adapter);
}
@Override
protected void onStart() {
super.onStart();
disposable.add(viewModel.concertList
.subscribe(adapter.submitList(flowableList)
));
}
@Override
protected void onStop() {
super.onStop();
disposable.clear();
}
}
不管是基于 RxJava2 的解决方案还是基于 LiveData 的解决方案, ConcertDao
和 ConcertAdapter
的代码是一样的。
本节以上面的内容为基础,描述了如何在应用 UI 中向用户展示 List,尤其是当 List 信息发生变化时。
你可以将 LiveData<PagedList>
的实例连接到 PagedListAdapter,就像下面一样
public class ConcertActivity extends AppCompatActivity {
private ConcertAdapter adapter = new ConcertAdapter();
private ConcertViewModel viewModel;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = ViewModelProviders.of(this).get(ConcertViewModel.class);
viewModel.concertList.observe(this, adapter::submitList);
}
}
当数据源(LiveData<PagedList<Value>>
)中的 PagedList 有新实例时,activity 会将这些对象发送给适配器。实现了 PagedListAdapter 的类定义了如何计算更新,并自动处理分页和列表差异。因此,我们的 ViewHolder 只需要绑定到特定的提供 Item :
public class ConcertAdapter
extends PagedListAdapter<Concert, ConcertViewHolder> {
protected ConcertAdapter() {
super(DIFF_CALLBACK);
}
@Override
public void onBindViewHolder(@NonNull ConcertViewHolder holder,
int position) {
Concert concert = getItem(position);
// Note that "concert" can be null if it's a placeholder.
holder.bindTo(concert);
}
private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK
= ... // See Implement the diffing callback section.
}
PagedListAdapter 使用 PagedList.Callback 对象来处理分页加载事件。当用户滑动列表时,PagedListAdapter 会调用 PagedList.loadAround() 为底层的 PagedList 提供关于它应从 DataSource 中获取哪些 items 的提示 。
注意:PagedList 是内容不可变的。这意味着,虽然可以将新内容加载到 PagedList 的实例中,但加载的 items 本身一旦加载就无法更改。因此,如果要更新 PagedList 中的内容,则 PagedListAdapter 对象会接收到包含更新信息的全新内容。
以下例子显示了 areContentsTheSame()
的实现,它比较了相关的对象字段:
private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
new DiffUtil.ItemCallback<Concert>() {
@Override
public boolean areItemsTheSame(Concert oldItem, Concert newItem) {
// The ID property identifies when items are the same.
return oldItem.getId() == newItem.getId();
}
@Override
public boolean areContentsTheSame(Concert oldItem, Concert newItem) {
// Don't use the "==" operator here. Either implement and use .equals(),
// or write custom data comparison logic here.
return oldItem.equals(newItem);
}
};
由于适配器比较项的定义,因此适配器会在加载新的 PagedList 对时自动检测对这些项的更改。因此,适配器会在 RecyclerView 对象中出发高效的动画。
如果你不继承 PagedListAdapter,例如当你使用自己的适配器时,你仍然可以通过直接使用 AsyncPagedListDiffer 对象来使用 Paging Library 适配器的 diffing 功能 。
如果你希望 UI 在应用完成获取数据之前就显示列表,你可以向用户显示占位符列表项。PagedList 通过将列表项数据显示为 null 来处理这样的情况,知道数据加载。
默认情况下,Paging 库启用占位符
占位符有以下好处:
在添加占位符支持之前,需要记住以下几点:
这节内容以 Paging 库为基础,讨论如何自定义程序的数据加载方法以满足应用的需求。
通常,我们的UI代码会观察一个 LiveData< PagedList>
对象(如果是 RxJava2,则是一个 Flowable<PagedList>
或 Observable<PagedList>
对象),它位于我们的应用程序中的 ViewModel 中。可观察对象将 App 列表数据的表示和内容连接在了一起。
为了创建可观察 PagedList 对象,需要将 DataSource.Factory
的实例传递给 LivePagedListBuilder
或 RxPagedListBuilder
对象。一个 DataSource 对象加载单个PagedList 的页面。工厂类创建 PagedList 的新实例 以响应内容更新,例如数据库表失效和网络刷新。Room 数据库 可以为我们提供 DataSource.Factory
对象,或者我们也可以构建自己的对象。
以下代码段展示如何使用 Room 的 DataSource.Factory
构建功能 在 App 的 ViewModel 类中创建 LiveData< PagedList>
的新实例:
ConcertDao
@Dao
public interface ConcertDao {
// The Integer type parameter tells Room to use a PositionalDataSource
// object, with position-based loading under the hood.
@Query("SELECT * FROM concerts ORDER BY date DESC")
DataSource.Factory<Integer, Concert> concertsByDate();
}
ConcertViewModel
// The Integer type argument corresponds to a PositionalDataSource object.
DataSource.Factory<Integer, Concert> myConcertDataSource =
concertDao.concertsByDate();
LiveData<PagedList<Concert>> concertList =
LivePagedListBuilder(myConcertDataSource, /* page size */ 50).build();
想要为高级用例进一步配置 LiveData< PagedList>
,我们还可以定义自己的分页配置。特别是,我们可以定义以下属性:
如果我们希望更好地控制 Paging 库何时从 App 的数据库加载列表,需要将自定义 Executor 对象传递给 LivePagedListBuilder,如下面的代码片段所示:
ConcertViewModel
PagedList.Config myPagingConfig = new PagedList.Config.Builder()
.setPageSize(50)
.setPrefetchDistance(150)
.setEnablePlaceholders(true)
.build();
// The Integer type argument corresponds to a PositionalDataSource object.
DataSource.Factory<Integer, Concert> myConcertDataSource =
concertDao.concertsByDate();
LiveData<PagedList<Concert>> concertList =
new LivePagedListBuilder<>(myConcertDataSource, myPagingConfig)
.setFetchExecutor(myExecutor)
.build();
连接到处理源数据结构的数据源非常重要:
当使用 Paging Library 时,如果数据过期或失效,由数据层通知应用程序的其他层。为此,我们可以选择调用 DataSource 的 invalidate() 方法。
注意:应用程序的UI可以使用滑动刷新触发此数据失效功能。
如果我们使用自定义本地数据的解决方案,或者直接从网络加载数据,则可以实现 DataSource 的一个子类。以下代码显示了一个数据源,该数据源取决于给定Concert的开始时间:
public class ConcertTimeDataSource
extends ItemKeyedDataSource<Date, Concert> {
@NonNull
@Override
public Date getKey(@NonNull Concert item) {
return item.getStartTime();
}
@Override
public void loadInitial(@NonNull LoadInitialParams<Date> params,
@NonNull LoadInitialCallback<Concert> callback) {
List<Concert> items =
fetchItems(params.key, params.requestedLoadSize);
callback.onResult(items);
}
@Override
public void loadAfter(@NonNull LoadParams<Date> params,
@NonNull LoadCallback<Concert> callback) {
List<Concert> items =
fetchItemsAfter(params.key, params.requestedLoadSize);
callback.onResult(items);
}
然后你可以通过创建 DataSource.Factory
的具体子类将该自定义数据加载到 PagedList 对象中。下面的代码显示了如何生成前面代码中定义的自定义数据源的新对象:
public class ConcertTimeDataSourceFactory
extends DataSource.Factory<Date, Concert> {
private MutableLiveData<ConcertTimeDataSource> sourceLiveData =
new MutableLiveData<>();
private ConcertDataSource latestSource;
@Override
public DataSource<Date, Concert> create() {
latestSource = new ConcertTimeDataSource();
sourceLiveData.postValue(latestSource);
return latestSource;
}
}
在构造可观察的 PagedList 对象时,请考虑内容更新的工作方式。如果您直接从 Room 中加载数据,则会自动将更新推送到应用的 UI。
使用分配网络 API 时,您通常会进行用户交互,例如“滑动刷新”,作为最近使用的数据源失效的信号。然后你再请求该数据源的新实例。下面的代码演示了这个操作:
public class ConcertActivity extends AppCompatActivity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
// ...
viewModel.getRefreshState()
.observe(this, new Observer<NetworkState>() {
// Shows one possible way of triggering a refresh operation.
@Override
public void onChanged(@Nullable MyNetworkState networkState) {
swipeRefreshLayout.isRefreshing =
networkState == MyNetworkState.LOADING;
}
};
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshListener() {
@Override
public void onRefresh() {
viewModel.invalidateDataSource();
}
});
}
}
public class ConcertTimeViewModel extends ViewModel {
private LiveData<PagedList<Concert>> concertList;
private DataSource<Date, Concert> mostRecentDataSource;
public ConcertTimeViewModel(Date firstConcertStartTime) {
ConcertTimeDataSourceFactory dataSourceFactory =
new ConcertTimeDataSourceFactory(firstConcertStartTime);
mostRecentDataSource = dataSourceFactory.create();
concertList = new LivePagedListBuilder<>(dataSourceFactory, 50)
.setFetchExecutor(myExecutor)
.build();
}
public void invalidateDataSource() {
mostRecentDataSource.invalidate();
}
}
Paging Library 支持 从 DataSource 加载基于 item 和基于页面 转换 的 item 。
在以下代码段中,Concert 名称和 Concert 日期的组合会被映射到包含名称和日期的单个字符串中:
public class ConcertViewModel extends ViewModel {
private LiveData<PagedList<String>> concertDescriptions;
public ConcertViewModel(MyDatabase database) {
DataSource.Factory<Integer, Concert> factory =
database.allConcertsFactory().map(concert ->
concert.getName() + "-" + concert.getDate());
concertDescriptions = new LivePagedListBuilder<>(
factory, /* page size */ 50).build();
}
}
如果我们要在加载 items 后对其进行封装,转换或准备,这可能很有用。由于此工作是在 executor(线程池)上完成的,因此我们可以执行可能一些代价高昂的工作,例如从磁盘读取或查询一个单独的数据库。
注意:JOIN 查询总是更高效,可以作为 map() 的一部分进行重新查询。