Quản lý và trình bày trạng thái tải

Thư viện phân trang theo dõi trạng thái của các yêu cầu tải dữ liệu được phân trang và hiển thị thông qua lớp LoadState. Ứng dụng có thể đăng ký trình xử lý với PagingDataAdapter để nhận thông tin về trạng thái hiện tại và cập nhật UI (giao diện người dùng) cho phù hợp. Bộ chuyển đổi cung cấp các trạng thái này vì các trạng thái đó đồng bộ với UI (giao diện người dùng). Điều này có nghĩa là trình nghe của bạn nhận được thông tin cập nhật khi UI (giao diện người dùng) áp dụng chế độ tải trang.

Bạn có thể cung cấp tín hiệu LoadState riêng biệt cho từng LoadType và loại nguồn dữ liệu (PagingSource hoặc RemoteMediator). Đối tượng CombinedLoadStates do trình nghe cung cấp cung cấp thông tin về trạng thái tải của tất cả tín hiệu này. Bạn có thể dùng thông tin chi tiết này để hiện các chỉ báo tải phù hợp cho người dùng.

Các trạng thái đang tải

Thư viện phân trang hiện trạng thái tải để sử dụng trong UI (giao diện người dùng) thông qua đối tượng LoadState. Các đối tượng LoadState có một trong ba dạng tuỳ thuộc vào trạng thái tải hiện tại:

  • Nếu không có hoạt động tải nào đang hoạt động và không có lỗi, thì LoadState là đối tượng LoadState.NotLoading. Lớp con này cũng bao gồm thuộc tính endOfPaginationReached, cho biết liệu đã đến cuối quá trình phân trang hay chưa.
  • Nếu có một hoạt động tải đang hoạt động, thì LoadState là đối tượng LoadState.Loading.
  • Nếu xảy ra lỗi thì LoadState là đối tượng LoadState.Error.

Có 2 cách để sử dụng LoadState trong UI (giao diện người dùng): dùng trình nghe hoặc dùng bộ chuyển đổi danh sách đặc biệt để trình bày trạng thái tải ngay trong danh sách RecyclerView.

Truy cập trạng thái đang tải bằng một trình nghe

Để nhận trạng thái đang tải cho mục đích sử dụng chung trong UI (giao diện người dùng) của bạn, hãy sử dụng luồng loadStateFlow hoặc phương thức addLoadStateListener() do PagingDataAdapter cung cấp. Các cơ chế này cung cấp quyền truy cập vào đối tượng CombinedLoadStates bao gồm thông tin về hành vi LoadState cho từng loại tải.

Trong ví dụ sau, PagingDataAdapter hiện các thành phần UI (giao diện người dùng) tuỳ thuộc vào trạng thái hiện tại của quá trình tải làm mới:

Kotlin

// Activities can use lifecycleScope directly, but Fragments should instead use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    progressBar.isVisible = loadStates.refresh is LoadState.Loading
    retry.isVisible = loadState.refresh !is LoadState.Loading
    errorMsg.isVisible = loadState.refresh is LoadState.Error
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.VISIBLE : View.GONE);
  retry.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.GONE : View.VISIBLE);
  errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error
    ? View.VISIBLE : View.GONE);
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.VISIBLE : View.GONE);
  retry.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.GONE : View.VISIBLE);
  errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error
    ? View.VISIBLE : View.GONE);
});

Để biết thêm thông tin về CombinedLoadStates, hãy xem Truy cập các thông tin bổ sung về trạng thái tải.

Trình bày trạng thái đang tải bằng một bộ chuyển đổi

Thư viện phân trang cung cấp một bộ chuyển đổi danh sách khác, có tên là LoadStateAdapter nhằm mục đích trình bày trạng thái tải ngay trong danh sách hiện của dữ liệu được phân trang. Bộ chuyển đổi này cho phép truy cập vào trạng thái tải hiện tại của danh sách, bạn có thể chuyển trạng thái này đến chế độ xem chủ sở hữu tuỳ chỉnh hiện thông tin này.

Đầu tiên, hãy tạo một lớp chế độ xem chủ sở hữu giúp giữ lại các thông tin tham chiếu đến chế độ xem đang tải và lỗi trên màn hình. Tạo một hàm bind() chấp nhận LoadState dưới dạng tham số. Hàm này sẽ bật/tắt chế độ xem hiển thị dựa trên tham số trạng thái tải:

Kotlin

class LoadStateViewHolder(
  parent: ViewGroup,
  retry: () -> Unit
) : RecyclerView.ViewHolder(
  LayoutInflater.from(parent.context)
    .inflate(R.layout.load_state_item, parent, false)
) {
  private val binding = LoadStateItemBinding.bind(itemView)
  private val progressBar: ProgressBar = binding.progressBar
  private val errorMsg: TextView = binding.errorMsg
  private val retry: Button = binding.retryButton
    .also {
      it.setOnClickListener { retry() }
    }

  fun bind(loadState: LoadState) {
    if (loadState is LoadState.Error) {
      errorMsg.text = loadState.error.localizedMessage
    }

    progressBar.isVisible = loadState is LoadState.Loading
    retry.isVisible = loadState is LoadState.Error
    errorMsg.isVisible = loadState is LoadState.Error
  }
}

Java

class LoadStateViewHolder extends RecyclerView.ViewHolder {
  private ProgressBar mProgressBar;
  private TextView mErrorMsg;
  private Button mRetry;

  LoadStateViewHolder(
    @NonNull ViewGroup parent,
    @NonNull View.OnClickListener retryCallback) {
    super(LayoutInflater.from(parent.getContext())
      .inflate(R.layout.load_state_item, parent, false));

    LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView);
    mProgressBar = binding.progressBar;
    mErrorMsg = binding.errorMsg;
    mRetry = binding.retryButton;
  }

  public void bind(LoadState loadState) {
    if (loadState instanceof LoadState.Error) {
      LoadState.Error loadStateError = (LoadState.Error) loadState;
      mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
    }
    mProgressBar.setVisibility(loadState instanceof LoadState.Loading
      ? View.VISIBLE : View.GONE);
    mRetry.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
    mErrorMsg.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
  }
}

Java

class LoadStateViewHolder extends RecyclerView.ViewHolder {
  private ProgressBar mProgressBar;
  private TextView mErrorMsg;
  private Button mRetry;

  LoadStateViewHolder(
    @NonNull ViewGroup parent,
    @NonNull View.OnClickListener retryCallback) {
    super(LayoutInflater.from(parent.getContext())
      .inflate(R.layout.load_state_item, parent, false));

    LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView);
    mProgressBar = binding.progressBar;
    mErrorMsg = binding.errorMsg;
    mRetry = binding.retryButton;
  }

  public void bind(LoadState loadState) {
    if (loadState instanceof LoadState.Error) {
      LoadState.Error loadStateError = (LoadState.Error) loadState;
      mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
    }
    mProgressBar.setVisibility(loadState instanceof LoadState.Loading
      ? View.VISIBLE : View.GONE);
    mRetry.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
    mErrorMsg.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
  }
}

Tiếp theo, hãy tạo một lớp triển khai LoadStateAdapter, đồng thời xác định các phương thức onCreateViewHolder()onBindViewHolder(). Các phương thức này tạo một bản sao của chế độ xem chủ sở hữu tuỳ chỉnh và ràng buộc trạng thái tải liên quan.

Kotlin

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter(
  private val retry: () -> Unit
) : LoadStateAdapter<LoadStateViewHolder>() {

  override fun onCreateViewHolder(
    parent: ViewGroup,
    loadState: LoadState
  ) = LoadStateViewHolder(parent, retry)

  override fun onBindViewHolder(
    holder: LoadStateViewHolder,
    loadState: LoadState
  ) = holder.bind(loadState)
}

Java

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> {
  private View.OnClickListener mRetryCallback;

  ExampleLoadStateAdapter(View.OnClickListener retryCallback) {
    mRetryCallback = retryCallback;
  }

  @NotNull
  @Override
  public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent,
    @NotNull LoadState loadState) {
    return new LoadStateViewHolder(parent, mRetryCallback);
  }

  @Override
  public void onBindViewHolder(@NotNull LoadStateViewHolder holder,
    @NotNull LoadState loadState) {
    holder.bind(loadState);
  }
}

Java

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> {
  private View.OnClickListener mRetryCallback;

  ExampleLoadStateAdapter(View.OnClickListener retryCallback) {
    mRetryCallback = retryCallback;
  }

  @NotNull
  @Override
  public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent,
    @NotNull LoadState loadState) {
    return new LoadStateViewHolder(parent, mRetryCallback);
  }

  @Override
  public void onBindViewHolder(@NotNull LoadStateViewHolder holder,
    @NotNull LoadState loadState) {
    holder.bind(loadState);
  }
}

Để hiển thị tiến trình tải trong mục đầu trang và chân trang, hãy gọi phương thức withLoadStateHeaderAndFooter() từ đối tượng PagingDataAdapter của bạn:

Kotlin

pagingAdapter
  .withLoadStateHeaderAndFooter(
    header = ExampleLoadStateAdapter(adapter::retry),
    footer = ExampleLoadStateAdapter(adapter::retry)
  )

Java

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

Java

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

Thay vào đó, bạn có thể gọi withLoadStateHeader() hoặc withLoadStateFooter() nếu bạn muốn danh sách RecyclerView hiện trạng thái tải chỉ trong mục đầu trang hoặc chỉ ở mục chân trang.

Truy cập các thông tin bổ sung về trạng thái đang tải

Đối tượng CombinedLoadStates của PagingDataAdapter cung cấp thông tin về các trạng thái tải cho quá trình triển khai PagingSource và cũng cho quá trình triển khai RemoteMediator của bạn (nếu có).

Để thuận tiện, bạn có thể sử dụng các thuộc tính refresh, appendprepend của CombinedLoadStates để truy cập vào đối tượng LoadState cho loại tải phù hợp. Các thuộc tính này thường tuân theo trạng thái tải của quá trình triển khai RemoteMediator (nếu có); nếu không, chúng chứa trạng thái tải phù hợp của quá trình triển khai PagingSource. Để biết thêm thông tin chi tiết về logic cơ bản, hãy xem tài liệu tham khảo dành cho CombinedLoadStates.

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Observe refresh load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    refreshLoadState: LoadState = loadStates.refresh
    // Observe prepend load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    prependLoadState: LoadState = loadStates.prepend
    // Observe append load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    appendLoadState: LoadState = loadStates.append
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

Tuy nhiên, bạn cần nhớ rằng chỉ các trạng thái tải PagingSource mới được đồng bộ hoá với các bản cập nhật UI (giao diện người dùng). Vì các thuộc tính refresh ,appendprepend có thể lấy trạng thái tải từ PagingSource hoặc RemoteMediator, nên chúng sẽ không được đảm bảo đồng bộ với các bản cập nhật UI (giao diện người dùng). Điều này có thể gây ra các sự cố về UI (giao diện người dùng) khi quá trình tải xuất hiện để hoàn thành trước khi bất kỳ dữ liệu mới nào được thêm vào UI (giao diện người dùng).

Vì lý do này, các trình truy cập tiện lợi hoạt động tốt để hiện trạng thái tải trong mục đầu trang hoặc chân trang, nhưng đối với các trường hợp sử dụng khác, bạn có thể cần truy cập cụ thể vào trạng thái tải của PagingSource hoặc RemoteMediator. CombinedLoadStates cung cấp các thuộc tính sourcemediator cho mục đích này. Mỗi thuộc tính này đều hiển thị một đối tượng LoadStates chứa các đối tượng LoadState cho PagingSource hoặc RemoteMediator tương ứng:

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Directly access the RemoteMediator refresh load state.
    mediatorRefreshLoadState: LoadState? = loadStates.mediator.refresh
    // Directly access the RemoteMediator append load state.
    mediatorAppendLoadState: LoadState? = loadStates.mediator.append
    // Directly access the RemoteMediator prepend load state.
    mediatorPrependLoadState: LoadState? = loadStates.mediator.prepend
    // Directly access the PagingSource refresh load state.
    sourceRefreshLoadState: LoadState = loadStates.source.refresh
    // Directly access the PagingSource append load state.
    sourceAppendLoadState: LoadState = loadStates.source.append
    // Directly access the PagingSource prepend load state.
    sourcePrependLoadState: LoadState = loadStates.source.prepend
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

Toán tử chuỗi trên LoadState

Vì đối tượng CombinedLoadStates cung cấp quyền truy cập vào tất cả thay đổi trong trạng thái tải, nên điều quan trọng là phải lọc luồng trạng thái tải dựa trên các sự kiện cụ thể. Điều này đảm bảo rằng bạn cập nhật UI (giao diện người dùng) vào thời điểm thích hợp để tránh tình trạng gián đoạn và cập nhật UI (giao diện người dùng) không cần thiết.

Ví dụ: giả sử bạn muốn hiện chế độ xem trống nhưng chỉ sau khi quá trình tải dữ liệu ban đầu hoàn tất. Trường hợp sử dụng này yêu cầu bạn xác minh rằng quá trình tải làm mới dữ liệu đã bắt đầu, sau đó đợi trạng thái NotLoading để xác nhận rằng quá trình làm mới đã hoàn tất. Bạn phải lọc ra tất cả tín hiệu ngoại trừ những tín hiệu bạn cần:

Kotlin

lifecycleScope.launchWhenCreated {
  adapter.loadStateFlow
    // Only emit when REFRESH LoadState for RemoteMediator changes.
    .distinctUntilChangedBy { it.refresh }
    // Only react to cases where REFRESH completes, such as NotLoading.
    .filter { it.refresh is LoadState.NotLoading }
    // Scroll to top is synchronous with UI updates, even if remote load was
    // triggered.
    .collect { binding.list.scrollToPosition(0) }
}

Java

PublishSubject<CombinedLoadStates> subject = PublishSubject.create();
Disposable disposable =
  subject.distinctUntilChanged(CombinedLoadStates::getRefresh)
  .filter(
    combinedLoadStates -> combinedLoadStates.getRefresh() instanceof LoadState.NotLoading)
  .subscribe(combinedLoadStates -> binding.list.scrollToPosition(0));

pagingAdapter.addLoadStateListener(loadStates -> {
  subject.onNext(loadStates);
});

Java

LiveData<CombinedLoadStates> liveData = new MutableLiveData<>();
LiveData<LoadState> refreshLiveData =
  Transformations.map(liveData, CombinedLoadStates::getRefresh);
LiveData<LoadState> distinctLiveData =
  Transformations.distinctUntilChanged(refreshLiveData);

distinctLiveData.observeForever(loadState -> {
  if (loadState instanceof LoadState.NotLoading) {
    binding.list.scrollToPosition(0);
  }
});

Ví dụ này sẽ chờ cho đến khi hệ thống cập nhật trạng thái tải làm mới, nhưng chỉ kích hoạt khi trạng thái là NotLoading. Điều này đảm bảo rằng quá trình làm mới từ xa đã hoàn tất trước khi cập nhật UI (giao diện người dùng).

API luồng giúp loại hoạt động này có thể thực hiện được. Ứng dụng của bạn có thể chỉ định các sự kiện tải cần thiết và xử lý dữ liệu mới khi đáp ứng các tiêu chí thích hợp.