로드 상태 관리 및 표시

Paging 라이브러리는 페이징된 데이터의 로드 요청 상태를 추적하고 LoadState 클래스를 통해 노출합니다. 앱은 PagingDataAdapter에 리스너를 등록하여 현재 상태에 관한 정보를 수신하고 그에 따라 UI를 업데이트할 수 있습니다. 이러한 상태는 UI와 동기화되므로 어댑터에서 제공됩니다. 즉, 페이지 로드가 UI에 적용되면 리스너가 업데이트를 수신합니다.

LoadType 및 데이터 소스 유형(PagingSource 또는 RemoteMediator)마다 별도의 LoadState 신호가 제공됩니다. 리스너에서 제공하는 CombinedLoadStates 객체는 이러한 모든 신호의 로드 상태에 관한 정보를 제공합니다. 이 세부정보를 사용하여 사용자에게 적절한 로드 표시기를 표시할 수 있습니다.

로드 상태

Paging 라이브러리는 LoadState 객체를 통해 UI에서 사용할 로드 상태를 노출합니다. LoadState 객체는 현재 로드 상태에 따라 다음 세 가지 형식 중 하나를 취합니다.

  • 활성 로드 작업이 없고 오류가 없는 경우 LoadStateLoadState.NotLoading 객체입니다. 이 서브클래스에는 페이지로 나누기의 끝에 도달했는지를 나타내는 endOfPaginationReached 속성이 포함됩니다.
  • 활성 로드 작업이 있는 경우 LoadStateLoadState.Loading 객체입니다.
  • 오류가 있는 경우 LoadStateLoadState.Error 객체입니다.

UI에서 LoadState를 사용하는 데는 두 가지 방법이 있습니다. 즉, 리스너를 사용하는 방법 또는 특수 목록 어댑터를 사용하여 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()를 대신 호출하면 됩니다.

추가 로드 상태 정보에 액세스하기

PagingDataAdapterCombinedLoadStates 객체는 PagingSource 구현과 RemoteMediator 구현(있는 경우)에 관한 로드 상태 정보를 제공합니다.

편의를 위해 CombinedLoadStatesrefresh, append, 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;
});

단, PagingSource 로드 상태만 UI 업데이트와 동기화된다는 점에 유의해야 합니다. refresh, append, prepend 속성은 잠재적으로 PagingSource 또는 RemoteMediator의 로드 상태를 사용할 수 있으므로 UI 업데이트와 동기화되지 않을 수도 있습니다. 이로 인해 새 데이터가 UI에 추가되기 전에 로드가 완료된 것으로 표시되는 UI 문제가 발생할 수 있습니다.

이러한 이유로 편의를 위해 제공되는 접근자는 머리글이나 바닥글에 로드 상태를 표시하는 데는 적합하지만, 다른 사용 사례를 위해서는 PagingSource 또는 RemoteMediator의 로드 상태에 명시적으로 액세스해야 할 수 있습니다. CombinedLoadStates는 이러한 용도를 위해 sourcemediator 속성을 제공합니다. 이러한 속성은 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 업데이트가 발생하기 전에 원격 새로고침이 완전히 완료됩니다.

Stream API를 사용하여 이러한 유형의 작업을 할 수 있습니다. 앱은 필요한 로드 이벤트를 지정할 수 있고 적절한 기준이 충족되면 새 데이터를 처리할 수 있습니다.