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
ListenableFuturetừ 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ụ dobackendchỉ đị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.
load() sử dụng và cập nhật khoá.
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ể
PagingDataAdaptervào danh sáchRecyclerViewmà bạn muốn hiện dữ liệu đã phân trang. - Quan sát luồng
PagingDatavà 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