Tải và hiển thị dữ liệu đã phân trang

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:

Chọn loại giá trị và khoá

PagingSource<Key, Value> có hai tham số loại: KeyValue. 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 KeyUser 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 KeyInt và loại ValueUser.

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ệu
  • query: cụm từ tìm kiếm gửi đến dịch vụ do backend 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.

Trên mỗi lệnh gọi tải(), ExamplePagingSource sẽ lấy khoá hiện tại
    và trả về khoá tiếp theo để tải.
Hình 1. Sơ đồ cho thấy cách 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 FlowableObservable 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()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:

  1. Tạo một thực thể của loại PagingDataAdapter.
  2. Chuyển thực thể PagingDataAdapter vào danh sách RecyclerView mà bạn muốn hiện dữ liệu đã phân trang.
  3. Quan sát luồng PagingData và chuyển từng giá trị đã tạo cho phương thức submitData() 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

Mẫu