Các khái niệm và cách triển khai Jetpack Compose
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
RxPagingSource. - Để sử dụng
ListenableFuturetừ Guava, hãy triển khaiListenableFuturePagingSource.
Chọn loại khoá và loại giá trị
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, hãy 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.
Java (RxJava)
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 (Guava/LiveData)
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 sẽ truyền các tham số được cung cấp trong hàm khởi tạo 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 sẽ được 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 lớp 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 triển khai một phương thức getRefreshKey lấy đối tượng PagingState làm 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:
Java (RxJava)
return backend.searchUsers(searchTerm, nextPageNumber)
.subscribeOn(Schedulers.io())
.map(this::toLoadResult)
.onErrorReturn(LoadResult.Error::new);
Java (Guava/LiveData)
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à trình bày 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:
Java (RxJava)
// 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 (Guava/LiveData)
// 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ụ này, UserAdapter mở rộng PagingDataAdapter để cung cấp một bộ chuyển đổi RecyclerView cho các mục trong danh sách thuộc loại User và sử dụng UserViewHolder làm trình lưu giữ thành phần hiển thị:
Kotlin (Coroutine)
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 (RxJava)
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 (Guava/LiveData)
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 (Coroutine)
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 (RxJava)
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 (Guava/LiveData)
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. - Truyền thực thể
PagingDataAdaptervào danh sáchRecyclerViewmà bạn muốn hiển thị 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 (Coroutine)
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 (RxJava)
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 (Guava/LiveData)
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.