分頁程式庫提供強大的功能,可載入並顯示大型資料集內的分頁資料。本指南說明如何使用分頁程式庫,設定來自網路資料來源的分頁資料串流,並顯示在 RecyclerView
中。
定義資料來源
首先,您必須定義 PagingSource
實作來識別資料來源。PagingSource
API 類別包含 load()
方法,您必須覆寫該方法,指出如何從對應的資料來源擷取分頁資料。
請直接透過 PagingSource
類別,使用 Kotlin 協同程式執行非同步載入。分頁程式庫也提供支援其他非同步架構的類別:
- 如要使用 RxJava,請改為導入
RxPagingSource
。 - 如要透過 Guava 使用
ListenableFuture
,請改為導入ListenableFuturePagingSource
。
選取鍵和值類型
PagingSource<Key, Value>
有兩個類型參數:Key
和 Value
。鍵會定義用於載入資料的 ID,值則是資料本身的類型。舉例來說,假設您將 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()
函式如何接收每個載入作業的鍵,並提供後續載入作業的鍵。
PagingSource
實作項目也必須實作 getRefreshKey()
方法,後者採用 PagingState
物件做為參數。當資料重新整理或在初始載入作業完成後失效時,這個方法會傳回要傳入 load()
方法的鍵。在後續重新整理資料時,分頁程式庫會自動呼叫這個方法。
處理錯誤
有很多原因會造成資料載入要求失敗,尤其是透過網路載入時。從 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
物件並傳送至 UI,方便您執行操作。如要進一步瞭解如何在 UI 中顯示載入狀態,請參閱「管理及顯示載入狀態」一文。
設定 PagingData 串流
接下來,您需要用到來自 PagingSource
實作的串流分頁資料。請在 ViewModel
中設定資料串流。Pager
類別提供的方法,可對來自 PagingSource
的 PagingData
物件顯示回應式串流。分頁程式庫支援使用多種串流類型,包括 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
清單中。為了達成這個目的,分頁程式庫提供了 PagingDataAdapter
類別。
定義擴充 PagingDataAdapter
的類別。在範例中,UserAdapter
擴充 PagingDataAdapter
以提供 User
類型的 RecyclerView
轉換介面清單項目,並使用 UserViewHolder
做為檢視畫面預留位置:
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
清單轉換介面時相同:
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
,你已經準備好將這些元素串連在一起,並在你的活動中顯示頁面資料。
在活動的 onCreate
或片段的 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 程式庫,請參閱以下資源:
程式碼研究室
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 從網路和資料庫進行分頁
- 遷移至 Paging 3
- Paging 程式庫總覽