Thư viện phân trang mang đến những tính năng đắc lực để tải và hiển thị
dữ liệu đã phân trang từ một tập dữ liệu lớn hơn. Hướng dẫn này minh hoạ cách sử dụng thư viện Phân trang
để thiết lập luồng dữ liệu đã phân trang từ một nguồn dữ liệu mạng
và hiển thị luồng đó trong RecyclerView
.
Xác định nguồn dữ liệu
Bước đầu tiên là xác định việc triển khai PagingSource
để xác định nguồn dữ liệu. Lớp API PagingSource
bao gồm phương thức load()
mà bạn phải ghi đè để cho biết cách truy xuất dữ liệu đã phân trang từ nguồn dữ liệu tương ứng.
Sử dụng trực tiếp lớp PagingSource
để dùng coroutine Kotlin cho quá trình tải không đồng bộ. Thư viện Phân trang cũng cung cấp nhiều loại để hỗ trợ các khung
không đồng bộ khác:
- Để sử dụng RxJava, hãy triển khai chức năng
RxPagingSource
. - Để sử dụng
ListenableFuture
từ Guava, hãy triển khaiListenableFuturePagingSource
.
Chọn loại giá trị và khoá
PagingSource<Key, Value>
có hai tham số loại: Key
và Value
. Khoá xác định giá trị nhận dạng dùng để tải dữ liệu và giá trị là loại của chính dữ liệu đó. Ví dụ: Nếu tải các trang của đối tượng User
từ mạng bằng cách chuyển số trang Int
sang Retrofit, bạn sẽ chọn Int
làm loại Key
và User
làm loại Value
.
Xác định PagingSource
Ví dụ sau đây triển khai một
PagingSource
tải
các trang của mục theo số trang. Loại Key
là Int
và loại Value
là 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; } }
Cách triển khai PagingSource
thông thường chuyển các tham số được cung cấp trong hàm dựng
sang phương thức load()
để tải dữ liệu thích hợp cho một truy vấn. Trong ví dụ trên, các tham số đó là:
backend
: một thực thể của dịch vụ phụ trợ cung cấp dữ liệuquery
: cụm từ tìm kiếm gửi đến dịch vụ dobackend
chỉ định
Đối tượng LoadParams
chứa thông tin về thao tác tải định thực hiện. Dữ liệu này bao gồm
khoá cần tải và số lượng mục cần tải.
Đối tượng LoadResult
chứa kết quả của thao tác tải. LoadResult
là một loại kín
ở một trong hai dạng, tuỳ vào lệnh gọi load()
có thành công hay không:
- Lần tải thành công trả về đối tượng
LoadResult.Page
. - Lần tải không thành công trả về đối tượng
LoadResult.Error
.
Hình dưới đây minh hoạ cách hàm load()
trong ví dụ này nhận khoá cho mỗi lần tải và cung cấp khoá cho lần tải tiếp theo.
Việc triển khai PagingSource
cũng phải thực thi phương thức getRefreshKey()
lấy đối tượng PagingState
làm một tham số. Phương thức này trả về khoá để chuyển vào phương thức load()
khi dữ liệu được làm mới hoặc không hợp lệ sau lần tải đầu tiên. Thư viện Paging tự động gọi phương thức này trong những lần làm mới dữ liệu tiếp theo.
Xử lý lỗi
Các yêu cầu tải dữ liệu có thể không thành công vì một số lý do, đặc biệt là khi tải qua mạng. Hãy báo cáo các lỗi gặp phải trong quá trình tải bằng cách trả về một đối tượng LoadResult.Error
từ phương thức load()
.
Ví dụ: Bạn có thể nắm bắt và báo cáo các lỗi tải trong ExamplePagingSource
từ ví dụ trước bằng cách thêm nội dung sau vào phương thức load()
:
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);
Để biết thêm thông tin về cách xử lý các lỗi Retrofit, hãy xem các mẫu trong Tài liệu tham khảo API PagingSource
.
PagingSource
thu thập và phân phối đối tượng LoadResult.Error
đến giao diện người dùng nên bạn có thể thao tác với các đối tượng đó. Để biết thêm thông tin về việc hiển thị trạng thái tải trong giao diện người dùng, hãy xem phần Quản lý và hiển thị trạng thái tải.
Thiết lập luồng PagingData
Tiếp theo, bạn cần một luồng dữ liệu đã phân trang từ lần triển khai PagingSource
.
Hãy thiết lập luồng dữ liệu này trong ViewModel
. Lớp Pager
cung cấp các phương thức hiển thị luồng phản ứng của đối tượng PagingData
từ một PagingSource
. Thư viện Phân trang hỗ trợ việc sử dụng một số loại luồng,
bao gồm Flow
, LiveData
và các loại Flowable
và Observable
từ
RxJava.
Khi tạo một thực thể Pager
để thiết lập luồng phản ứng, bạn phải
cung cấp thực thể đó với
một đối tượng cấu hình PagingConfig
và một hàm cho biết Pager
cách tải một thực thể của hoạt động triển khai
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);
Toán tử cachedIn()
giúp luồng dữ liệu có thể chia sẻ, đồng thời lưu dữ liệu đã tải vào bộ nhớ đệm với CoroutineScope
được cung cấp. Ví dụ này sử dụng viewModelScope
do cấu phần phần mềm vòng đời lifecycle-viewmodel-ktx
cung cấp.
Đối tượng Pager
gọi phương thức load()
từ đối tượng PagingSource
, cung cấp cùng với đối tượng LoadParams
và nhận đối tượng LoadResult
khi trả về.
Xác định bộ chuyển đổi RecyclerView
Bạn cũng cần thiết lập bộ chuyển đổi để nhận dữ liệu
vào danh sách RecyclerView
của mình. Thư viện Phân trang cung cấp loại PagingDataAdapter
cho mục đích này.
Xác định một loại mở rộng PagingDataAdapter
. Trong ví dụ,
UserAdapter
mở rộng PagingDataAdapter
để cung cấp một bộ chuyển đổi RecyclerView
cho các mục danh sách thuộc loại User
và sử dụng UserViewHolder
làm chế độ xem chủ sở hữu:
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); } }
Bộ chuyển đổi của bạn cũng phải xác định
các phương thức onCreateViewHolder()
và onBindViewHolder()
,
rồi chỉ định một DiffUtil.ItemCallback
.
Phương thức này hoạt động giống hệt cách thông thường như
khi xác định các bộ chuyển đổi danh sách 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); } }
Hiển thị dữ liệu đã phân trang trong giao diện người dùng
Bây giờ, bạn đã xác định được PagingSource
, tạo một cách để ứng dụng của bạn
tạo luồng PagingData
và xác định PagingDataAdapter
, bạn đã sẵn sàng
kết nối các phần tử này với nhau
và hiển thị dữ liệu đã phân trang trong hoạt động của mình.
Thực hiện theo các bước sau trong phương thức onCreate
của hoạt động
hoặc phương thức onViewCreated
của hoặc phân đoạn:
- Tạo một thực thể của loại
PagingDataAdapter
. - Chuyển thực thể
PagingDataAdapter
vào danh sáchRecyclerView
mà bạn muốn hiện dữ liệu đã phân trang. - Quan sát luồng
PagingData
và chuyển từng giá trị đã tạo cho phương thứcsubmitData()
của bộ chuyển đổi mà bạn có.
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));
Danh sách RecyclerView
hiện hiển thị dữ liệu đã phân trang từ nguồn dữ liệu
và tự động tải một trang khác khi cần.
Tài nguyên khác
Để tìm hiểu thêm về thư viện Paging, hãy xem các tài nguyên khác sau đây:
Lớp học lập trình
Đề xuất cho bạn
- Lưu ý: văn bản liên kết sẽ hiện khi JavaScript tắt
- Phân trang qua mạng và cơ sở dữ liệu
- Di chuyển sang Paging 3
- Tổng quan về thư viện Paging