読み込み状態の管理と表示

Paging ライブラリは、ページング データの読み込みリクエストの状態をトラッキングし、LoadState クラスを通じて公開します。アプリは PagingDataAdapter にリスナーを登録して現在の状態に関する情報を受信し、それに応じて UI を更新できます。これらの状態は、UI と同期しているため、アダプターから提供されます。つまり、ページ読み込みが UI に適用されると、リスナーは更新情報を受信します。

個々の LoadState シグナルは、LoadType とデータソース タイプ(PagingSource または RemoteMediator)ごとに提供されます。リスナーから渡される CombinedLoadStates オブジェクトは、これらすべてのシグナルから届いた読み込み状態に関する情報を提供します。この詳細な情報を使用して、適切な読み込みインジケーターをユーザーに表示できます。

読み込み状態

Paging ライブラリは、LoadState オブジェクトを介して、UI で使用される読み込み状態を公開します。LoadState オブジェクトは、現在の読み込み状態に応じて次の 3 つの形態のいずれかを取ります。

  • アクティブな読み込みオペレーションが存在せず、エラーがない場合、LoadStateLoadState.NotLoading オブジェクトになります。このサブクラスには、ページネーションが終わりに達したかどうかを示す endOfPaginationReached プロパティも含まれています。
  • アクティブな読み込みオペレーションが存在する場合、LoadStateLoadState.Loading オブジェクトになります。
  • エラーがある場合、LoadStateLoadState.Error オブジェクトになります。

UI で LoadState を使用するには、2 つの方法があります。つまり、リスナーを使用する方法と、特別なリスト アダプターを使って読み込み状態を直接 RecyclerView リストに表示する方法です。

リスナーを使用して読み込み状態にアクセスする

UI で一般的に使用される読み込み状態を取得するには、loadStateFlow ストリームを使用するか、PagingDataAdapter によって提供される addLoadStateListener() メソッドを使用します。これらのメカニズムにより、各読み込みタイプの LoadState 動作に関する情報を含む CombinedLoadStates オブジェクトへのアクセスが提供されます。

次の例の PagingDataAdapter は、更新読み込みの現在の状態に応じて、異なる UI コンポーネントを表示します。

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

CombinedLoadStates について詳しくは、その他の読み込み状態の情報にアクセスするをご覧ください。

アダプターで読み込み状態を表示する

Paging ライブラリには、LoadStateAdapter という名前の別のリスト アダプターがあります。これは、表示されるページング データのリストに読み込み状態を直接表示するために使用します。このアダプターは、リストの現在の読み込み状態へのアクセスを提供します。これを、情報を表示するカスタム ビューホルダーに渡すことができます。

まず、画面上の読み込みビューとエラービューへの参照を保持するビューホルダー クラスを作成します。LoadState をパラメータとして受け取る bind() 関数を作成します。この関数は、読み込み状態パラメータに基づいて、ビューの表示を切り替えます。

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

次に、LoadStateAdapter を実装するクラスを作成し、onCreateViewHolder() メソッドと onBindViewHolder() メソッドを定義します。これらのメソッドは、カスタム ビューホルダーのインスタンスを作成し、関連する読み込み状態をバインドします。

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

ヘッダーとフッターに読み込みの進行状況を表示するには、PagingDataAdapter オブジェクトから withLoadStateHeaderAndFooter() メソッドを呼び出します。

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

代わりに、RecyclerView リストでヘッダーのみ、またはフッターのみに読み込み状態を表示したい場合は、withLoadStateHeader() または withLoadStateFooter() を呼び出すこともできます。

その他の読み込み状態の情報にアクセスする

PagingDataAdapter からの CombinedLoadStates オブジェクトは、PagingSource 実装と、RemoteMediator 実装(存在する場合)の読み込み状態に関する情報を提供します。

簡便化のため、CombinedLoadStates からの refreshappend および prepend プロパティを使用して、適切な読み込みタイプに応じた LoadState オブジェクトにアクセスできます。一般的に、これらのプロパティは、RemoteMediator 実装からの読み込み状態が存在する場合はそれに従います。そうでない場合は、PagingSource 実装からの適切な読み込み状態を保持します。基盤となるロジックについて詳しくは、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;
});

なお、UI 更新との同期が保証されるのは、PagingSource 読み込み状態のみであることにご注意ください。refreshappend、および prepend プロパティは、PagingSource または RemoteMediator のいずれかから読み込み状態を取得する可能性があるため、UI 更新との同期は保証されません。そのため、新しいデータが UI に追加される前に、UI で読み込みが完了したように見える問題が発生する可能性があります。

したがって、コンビニエンス アクセサは読み込み状態をヘッダーまたはフッターに表示する場合は適切に機能しますが、他のユースケースでは、明確に PagingSource または RemoteMediator のいずれかから読み込み状態にアクセスする必要があります。CombinedLoadStates は、この目的のために source プロパティと mediator プロパティを提供します。これらのプロパティは、PagingSource または RemoteMediator 用の LoadState オブジェクトを含む LoadStates オブジェクトをそれぞれ公開します。

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

LoadState のチェーン演算子

CombinedLoadStates オブジェクトは読み込み状態のあらゆる変更へのアクセスを提供するので、特定のイベントに基づいて読み込み状態ストリームをフィルタすることが重要です。これにより、適切なタイミングで UI を更新して、ぎこちない動作や不要な UI 更新を回避できます。

たとえば、初期データの読み込みが完了した後で初めて空のビューを表示したいとします。このユースケースでは、データ更新読み込みが開始されたことを確認してから、NotLoading 状態を待機して更新の完了を確認することが求められます。必要なものを除くすべてのシグナルをフィルタで除外する必要があります。

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

この例では、更新の読み込み状態が更新されるまで待機しますが、状態が NotLoading の場合にのみトリガーします。これにより、UI 更新が発生する前にリモート更新が完全に終了することが保証されます。

ストリーム API を使用すると、このようなオペレーションが可能になります。アプリでは、アプリに必要な読み込みイベントを指定して、適切な条件が満たされたときに新しいデータを処理できます。