Mengelola dan menampilkan status pemuatan

Libary Paging melacak status permintaan pemuatan untuk data yang di-page dan menampilkannya melalui class LoadState. Aplikasi Anda dapat mendaftarkan pemroses dengan PagingDataAdapter untuk menerima informasi tentang status saat ini dan mengupdate UI sebagaimana mestinya. Status ini disediakan dari adaptor karena status tersebut sinkron dengan UI. Ini berarti bahwa pemroses Anda menerima update saat pemuatan halaman telah diterapkan ke UI.

Sinyal LoadState terpisah diberikan untuk setiapLoadType dan jenis sumber data (PagingSource atau RemoteMediator). Objek CombinedLoadStates yang diberikan oleh pemroses berisi informasi tentang status pemuatan dari semua sinyal ini. Anda dapat menggunakan informasi mendetail ini untuk menampilkan indikator pemuatan yang sesuai untuk pengguna Anda.

Status pemuatan

Library Paging menampilkan status pemuatan untuk digunakan di UI melalui objek LoadState. Objek LoadState mengambil salah satu dari tiga bentuk, bergantung pada status pemuatan saat ini:

  • Jika tidak ada operasi pemuatan yang aktif dan tidak ada error, LoadState adalah objek LoadState.NotLoading. Subclass ini juga menyertakan properti endOfPaginationReached, yang menunjukkan apakah akhir penomoran halaman telah tercapai.
  • Jika ada operasi pemuatan yang aktif, LoadState adalah objek LoadState.Loading.
  • Jika ada error, LoadState adalah objek LoadState.Error.

Ada dua cara untuk menggunakan LoadState di UI Anda: menggunakan pemroses, atau menggunakan adaptor daftar khusus untuk menyajikan status pemuatan langsung dalam daftar RecyclerView.

Mengakses status pemuatan dengan pemroses

Untuk mendapatkan status pemuatan bagi penggunaan umum di UI Anda, gunakan aliran loadStateFlow atau metode addLoadStateListener() yang disediakan oleh PagingDataAdapter Anda. Mekanisme ini menyediakan akses ke objek CombinedLoadStates yang menyertakan informasi tentang perilaku LoadState untuk setiap jenis pemuatan.

Dalam contoh berikut, PagingDataAdapter menampilkan komponen UI yang berbeda, bergantung pada status pemuatan refresh saat ini:

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);
});

Untuk informasi selengkapnya tentang CombinedLoadStates, baca Mengakses informasi status pemuatan tambahan.

Menampilkan status pemuatan dengan adaptor

Library Paging menyediakan adaptor daftar lain yang disebut LoadStateAdapter untuk menyajikan status pemuatan langsung dalam daftar data yang di-page yang ditampilkan. Adaptor ini memberikan akses ke status pemuatan daftar saat ini, yang dapat Anda teruskan ke holder tampilan kustom yang menampilkan informasi tersebut.

Pertama-tama, buat class holder tampilan yang menyimpan referensi ke tampilan pemuatan dan error pada layar Anda. Buat fungsi bind() yang menerima LoadState sebagai parameter. Fungsi ini harus mengalihkan visibilitas tampilan berdasarkan parameter status pemuatan:

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);
  }
}

Selanjutnya, buat class yang mengimplementasikan LoadStateAdapter, dan tentukan metode onCreateViewHolder() dan onBindViewHolder(). Metode tersebut membuat instance view holder kustom Anda dan mengikat status pemuatan terkait.

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);
  }
}

Untuk menampilkan proses pemuatan di header dan footer, panggil metode withLoadStateHeaderAndFooter() dari objek PagingDataAdapter Anda.

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));

Sebagai gantinya, Anda dapat memanggil withLoadStateHeader() atau withLoadStateFooter() jika Anda ingin daftar RecyclerView menampilkan status pemuatan hanya di header atau hanya di footer.

Mengakses informasi status pemuatan tambahan

Objek CombinedLoadStates dari PagingDataAdapter menyediakan informasi tentang status pemuatan untuk implementasi PagingSource serta untuk implementasi RemoteMediator Anda, jika ada.

Agar mudah, Anda dapat menggunakan properti refresh, append, dan prepend dari CombinedLoadStates guna mengakses objek LoadState untuk jenis pemuatan yang sesuai. Properti ini biasanya berbeda dari status pemuatan dari implementasi RemoteMediator, jika ada; jika tidak, properti tersebut berisi status pemuatan yang sesuai dari implementasi PagingSource. Untuk informasi lebih mendetail tentang logika yang mendasarinya, baca dokumentasi referensi untuk 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;
});

Namun, penting untuk diingat bahwa hanya status pemuatan PagingSource yang dijamin akan sinkron dengan update UI. Karena properti refresh, append, dan prepend berpotensi dapat mengambil status pemuatan dari PagingSource atau RemoteMediator, properti tersebut tidak dijamin akan sinkron dengan update UI. Hal ini dapat menyebabkan masalah UI ketika pemuatan telah selesai sebelum data baru apa pun ditambahkan ke UI tersebut.

Karena alasan ini, aksesor praktis berfungsi dengan baik untuk menampilkan status pemuatan di header atau footer. Namun, untuk kasus penggunaan lain, Anda mungkin perlu secara spesifik mengakses status pemuatan dari PagingSource atau RemoteMediator. CombinedLoadStates menyediakan properti source dan mediator untuk tujuan ini. Setiap properti ini menampilkan objek LoadStates yang berisi objek LoadState, masing-masing untuk PagingSource atau RemoteMediator:

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;
});

Operator rantai di LoadState

Karena objek CombinedLoadStates memberikan akses ke semua perubahan di status pemuatan, penting untuk memfilter aliran status pemuatan berdasarkan peristiwa tertentu. Hal ini memastikan bahwa Anda mengupdate UI pada saat yang tepat untuk menghindari update UI yang tersendat dan tidak penting.

Misalnya, anggaplah Anda ingin menampilkan tampilan kosong tetapi hanya setelah pemuatan data awal selesai. Kasus penggunaan ini mengharuskan Anda memastikan bahwa pemuatan refresh data telah dimulai, lalu tunggu status NotLoading untuk mengonfirmasi bahwa refresh tersebut telah selesai. Anda harus memfilter semua sinyal, kecuali yang Anda perlukan:

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);
  }
});

Contoh ini menunggu hingga status pemuatan refresh diupdate, tetapi hanya memicu ketika statusnya NotLoading. Cara ini memastikan bahwa refresh jarak jauh telah selesai sepenuhnya sebelum update UI apa pun dilakukan.

Stream API yang memungkinkan jenis operasi ini. Aplikasi Anda dapat menentukan peristiwa pemuatan yang diperlukannya dan menangani data baru saat kriteria yang sesuai telah terpenuhi.