Paging 库提供了强大的功能,用于从较大的数据集加载和显示分页数据。本指南将演示如何使用 Paging 库设置来自网络数据源的分页数据流并将其显示在 RecyclerView
中。
定义数据源
第一步是定义用于标识数据源的 PagingSource
实现。PagingSource
API 类包含 load()
方法,您需要替换该方法,以指明如何从相应数据源检索分页数据。
直接使用 PagingSource
类即可通过 Kotlin 协程进行异步加载。Paging 库还提供了支持其他异步框架的类:
- 如需使用 RxJava,请改为实现
RxPagingSource
。 - 如需使用 Guava 中的
ListenableFuture
,请改为实现ListenableFuturePagingSource
。
选择键和值类型
PagingSource<Key, Value>
有两种类型参数:Key
和 Value
。键定义了用于加载数据的标识符,值是数据本身的类型。例如,如果您通过将 Int
页码传递给 Retrofit 来从网络加载各页 User
对象,则应选择 Int
作为 Key
类型,选择 User
作为 Value
类型。
定义 PagingSource
下面的示例实现了按页码加载各页对象的 PagingSource
。Key
类型为 Int
,Value
类型为 User
。
Kotlin
class ExamplePagingSource( val backend: ExampleBackendService, val query: String ) : PagingSource<Int, User>() { override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> { try { // Start refresh at page 1 if undefined. val nextPageNumber = params.key ?: 1 val response = backend.searchUsers(query, nextPageNumber) return LoadResult.Page( data = response.users, prevKey = null, // Only paging forward. nextKey = response.nextPageNumber ) } catch (e: Exception) { // Handle errors in this block and return LoadResult.Error for // expected errors (such as a network failure). } } override fun getRefreshKey(state: PagingState<Int, User>): Int? { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } } }
Java
class ExamplePagingSource extends RxPagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; ExamplePagingSource(@NonNull ExampleBackendService backend, @NonNull String query) { mBackend = backend; mQuery = query; } @NotNull @Override public Single<LoadResult<Integer, User>> loadSingle( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } return mBackend.searchUsers(mQuery, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUserResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } @Nullable @Override public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. Integer anchorPosition = state.getAnchorPosition(); if (anchorPosition == null) { return null; } LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition); if (anchorPage == null) { return null; } Integer prevKey = anchorPage.getPrevKey(); if (prevKey != null) { return prevKey + 1; } Integer nextKey = anchorPage.getNextKey(); if (nextKey != null) { return nextKey - 1; } return null; } }
Java
class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; @NonNull private Executor mBgExecutor; ExamplePagingSource( @NonNull ExampleBackendService backend, @NonNull String query, @NonNull Executor bgExecutor) { mBackend = backend; mQuery = query; mBgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<LoadResult<Integer, User>> loadFuture(@NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform(mBackend.searchUsers(mQuery, nextPageNumber), this::toLoadResult, mBgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching(pageFuture, HttpException.class, LoadResult.Error::new, mBgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, mBgExecutor); } private LoadResult<Integer, User> toLoadResult(@NonNull SearchUserResponse response) { return new LoadResult.Page<>(response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } @Nullable @Override public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) { // Try to find the page key of the closest page to anchorPosition from // either the prevKey or the nextKey; you need to handle nullability // here. // * prevKey == null -> anchorPage is the first page. // * nextKey == null -> anchorPage is the last page. // * both prevKey and nextKey are null -> anchorPage is the // initial page, so return null. Integer anchorPosition = state.getAnchorPosition(); if (anchorPosition == null) { return null; } LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition); if (anchorPage == null) { return null; } Integer prevKey = anchorPage.getPrevKey(); if (prevKey != null) { return prevKey + 1; } Integer nextKey = anchorPage.getNextKey(); if (nextKey != null) { return nextKey - 1; } return null; } }
典型的 PagingSource
实现会将其构造函数中提供的参数传递给 load()
方法,以便为查询加载适当的数据。在上面的示例中,这些参数包括:
backend
:提供数据的后端服务实例。query
:要发送到backend
指示的服务的搜索查询。
LoadParams
对象包含有关要执行的加载操作的信息,其中包括要加载的键和要加载的项数。
LoadResult
对象包含加载操作的结果。LoadResult
是一个密封的类,根据 load()
调用是否成功,采用如下两种形式之一:
- 如果加载成功,则返回
LoadResult.Page
对象。 - 如果加载失败,则返回
LoadResult.Error
对象。
下图说明了此示例中的 load()
函数如何接收每次加载的键并为后续加载提供键。
load()
如何使用和更新键的流程图。
PagingSource
实现还必须实现 getRefreshKey()
方法,该方法接受 PagingState
对象作为参数。当数据在初始加载后刷新或失效时,该方法会返回要传递给 load()
方法的键。在后续刷新数据时,Paging 库会自动调用此方法。
处理错误
数据加载请求�����������种���因而失败,特别是在通过网络加载时。通过从 load()
方法返回 LoadResult.Error
对象,可报告在加载过程中遇到的错误。
例如,对于上一个示例,您可以通过向 load()
方法添加以下内容来捕获和报告 ExamplePagingSource
中的加载错误:
Kotlin
catch (e: IOException) { // IOException for network failures. return LoadResult.Error(e) } catch (e: HttpException) { // HttpException for any non-2xx HTTP status codes. return LoadResult.Error(e) }
Java
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Java
ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( backend.searchUsers(query, nextPageNumber), this::toLoadResult, bgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, bgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, bgExecutor);
如需详细了解如何处理 Retrofit 错误,请参阅 PagingSource
API 参考文档中的示例。
PagingSource
会收集 LoadResult.Error
对象并将其传递给界面,以便您对其执行操作。如需详细了解如何在界面中显示加载状态,请参阅管理和显示加载状态。
设置 PagingData 流
接下来,您需要来自 PagingSource
实现的分页数据流。在 ViewModel
中设置数据流。Pager
类提供的方法可显示来自 PagingSource
的 PagingData
对象的响应式流。Paging 库支持使用多种流类型,包括 Flow
、LiveData
以及 RxJava 中的 Flowable
和 Observable
类型。
当您创建 Pager
实例来设置响应式流时,必须为实例提供 PagingConfig
配置对象和告知 Pager
如何获取 PagingSource
实现实例的函数:
Kotlin
val flow = Pager( // Configure how data is loaded by passing additional properties to // PagingConfig, such as prefetchDistance. PagingConfig(pageSize = 20) ) { ExamplePagingSource(backend, query) }.flow .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager); PagingRx.cachedIn(flowable, viewModelScope);
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
cachedIn()
运算符使数据流可共享,并使用提供的 CoroutineScope
缓存加载的数据。此示例使用生命周期 lifecycle-viewmodel-ktx
工件提供的 viewModelScope
。
Pager
对象会调用 PagingSource
对象的 load()
方法,为其提供 LoadParams
对象,并接收 LoadResult
对象作为交换。
定义 RecyclerView 适配器
您还需要设置一个适配器来将数据接收到 RecyclerView
列表中。为此,Paging 库提供了 PagingDataAdapter
类。
定义一个扩展 PagingDataAdapter
的类。在此示例���,UserAdapter
扩展了 PagingDataAdapter
,用于为 User
类型的列表项提供 RecyclerView
适配器,并使用 UserViewHolder
作为 ViewHolder:
Kotlin
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) : PagingDataAdapter<User, UserViewHolder>(diffCallback) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): UserViewHolder { return UserViewHolder(parent) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val item = getItem(position) // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item) } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item can be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
您的适配器还必须定义 onCreateViewHolder()
和 onBindViewHolder()
方法,并指定 DiffUtil.ItemCallback
。这与定义 RecyclerView
列表 Adapter 时的通常做法相同:
Kotlin
object UserComparator : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { // Id is unique. return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
在界面中显示分页数据
现在您已经定义了 PagingSource
,为您的应用创建了生成 PagingData
流的方法,还定义了 PagingDataAdapter
,接下来便可以将这些元素连接起来,在您的 Activity 中显示分页数据。
在 Activity 的 onCreate
或 Fragment 的 onViewCreated
方法中执行以下步骤:
- 创建
PagingDataAdapter
类的实例。 - 将
PagingDataAdapter
实例传递给您要显示分页数据的RecyclerView
列表。 - 观察
PagingData
流,并将生成的每个值传递给适配器的submitData()
方法。
Kotlin
val viewModel by viewModels<ExampleViewModel>() val pagingAdapter = UserAdapter(UserComparator) val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) recyclerView.adapter = pagingAdapter // Activities can use lifecycleScope directly; fragments use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { viewModel.flow.collectLatest { pagingData -> pagingAdapter.submitData(pagingData) } }
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter viewModel.flowable // Using AutoDispose to handle subscription lifecycle. // See: https://github.com/uber/AutoDispose. .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter // Activities can use getLifecycle() directly; fragments use // getViewLifecycleOwner().getLifecycle(). viewModel.liveData.observe(this, pagingData -> pagingAdapter.submitData(getLifecycle(), pagingData));
RecyclerView
列表现在会显示来自数据源的分页数据,并会在必要时自动加载另一个页面。
其他资源
如需详细了解 Paging 库,请参阅下列其他资源:
Codelab
为您推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- 从网络和数据库加载页面
- 迁移到 Paging 3
- Paging 库概览