페이징 데이터 로드 및 표시

페이징 라이브러리는 대규모 데이터 세트에서 페이징된 데이터를 로드하고 표시하는 강력한 기능을 제공합니다. 이 가이드에서는 페이징 라이브러리를 사용하여 네트워크 데이터 소스에서 페이징된 데이터의 스트림을 설정하고 RecyclerView에 표시하는 방법을 보여 줍니다.

데이터 소스 정의

첫 번째 단계는 데이터 소스를 식별하기 위해 PagingSource 구현을 정의하는 것입니다. PagingSource API 클래스에는 load() 메서드가 포함되어 있으며, 이 메서드는 상응하는 데이터 소스에서 페이징된 데이터를 검색하는 방법을 나타내기 위해 재정의해야 합니다.

PagingSource 클래스를 직접 사용하여 비동기 로드에 Kotlin 코루틴을 사용합니다. 페이징 라이브러리는 다른 비동기 프레임워크를 지원하는 클래스도 제공합니다.

키 및 값 유형 선택

PagingSource<Key, Value>에는 KeyValue의 두 유형 매개변수가 있습니다. 키는 데이터를 로드하는 데 사용되는 식별자를 정의하며, 값은 데이터 자체의 유형입니다. 예를 들어 Int 페이지 번호를 Retrofit에 전달하여 네트워크에서 User 객체의 페이지를 로드하는 경우 Key 유형으로 Int를, Value 유형으로 User를 선택합니다.

PagingSource 정의

다음 예에서는 페이지 번호를 기준으로 항목 페이지를 로드하는 PagingSource를 구현합니다. Key 유형은 Int이고 Value 유형은 User입니다.

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 if it is an
      // expected error (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, but you need to handle nullability
    // here:
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey null -> anchorPage is the initial page, so
    //    just return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

자바

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, but you need to handle nullability
    // here:
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey null -> anchorPage is the initial page, so
    //    just 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;
  }
}

자바

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, but you need to handle nullability
    // here:
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey null -> anchorPage is the initial page, so
    //    just 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;
  }
}

일반적인 PagingSource 구현은 생성자에서 제공된 매개변수를 load() 메서드에 전달하여 쿼리에 적절한 데이터를 로드합니다. 위의 예에서 이러한 매개변수는 다음과 같습니다.

  • backend: 데이터를 제공하는 백엔드 서비스의 인스턴스입니다.
  • query: backend로 표시된 서비스에 전송할 검색어입니다.

LoadParams 객체에는 실행할 로드 작업에 관한 정보가 포함됩니다. 여기에는 로드할 키와 로드할 항목 수가 포함됩니다.

LoadResult 객체에는 로드 작업의 결과가 포함됩니다. LoadResultload() 호출이 성공했는지 여부에 따라 두 가지 형식 중 하나를 취하는 봉인 클래스입니다.

  • 로드에 성공하면 LoadResult.Page 객체를 반환합니다.
  • 로드에 실패하면 LoadResult.Error 객체를 반환합니다.

그림 1에서는 이 예의 load() 함수가 각 로드의 키를 수신하고 후속 로드용 키를 제공하는 방법을 보여줍니다.

각 load() 호출에서 ExamplePagingSource는 현재 키를 가져와서 로드할 다음 키를 반환합니다.
그림 1. load()의 키 사용 및 업데이트 방법을 보여 주는 다이어그램

PagingSource 구현은 getRefreshKey() 메서드도 반드시 구현해야 하며 이는 PagingState 객체를 매개변수로 취하고 데이터가 새로고침되거나 첫 로드 후 무효화되었을 때 키를 반환하여 load()로 전달합니다. 페이징 라이브러리는 다음에 데이터를 새로고침할 때 자동으로 이 메서드를 호출합니다.

오류 처리

데이터 로드 요청은 특히 네트워크를 통해 로드하는 경우 여러 가지 이유로 실패할 수 있습니다. 로드하는 중에 load() 메서드에서 LoadResult.Error 객체를 반환하여 발생한 오류를 보고합니다.

예를 들어 load() 메서드에 다음을 추가하여 이전 예시의 ExamplePagingSource에서 로드 오류를 포착하여 보고할 수 있습니다.

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

자바

return backend.searchUsers(searchTerm, nextPageNumber)
  .subscribeOn(Schedulers.io())
  .map(this::toLoadResult)
  .onErrorReturn(LoadResult.Error::new);

자바

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

Retrofit 오류 처리에 관한 자세한 내용은 PagingSource API 참조의 샘플을 참고하세요.

PagingSource는 개발자가 조치를 취할 수 있도록 LoadResult.Error 객체를 수집하여 UI에 제공합니다. UI에 로드 상태를 노출하는 작업에 관한 자세한 내용은 로드 상태 표시를 참고하세요.

PagingData 스트림 설정

그런 다음 PagingSource 구현에서 페이징된 데이터의 스트림이 필요합니다. 일반적으로 ViewModel에서 데이터 스트림을 설정해야 합니다. Pager 클래스는 PagingSource에서 PagingData 객체의 반응형 스트림을 노출하는 메서드를 제공합니다. 페이징 라이브러리는 Flow, LiveData, RxJava의 Flowable 유형과 Observable 유형을 비롯한 여러 스트림 유형을 사용할 수 있도록 지원합니다.

Pager 스트림을 만들어 반응형 스트림을 설정할 때는 PagingConfig 구성 객체와 PagingSource 구현 인스턴스를 가져오는 방법을 Pager에 지시하는 함수를 인스턴스에 제공해야 합니다.

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)

자바

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

자바

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

cachedIn() 연산자는 데이터 스트림을 공유 가능하게 하며 제공된 CoroutineScope를 사용하여 로드된 데이터를 캐시합니다. 이 예에서는 Lifecycle lifecycle-viewmodel-ktx 아티팩트가 제공하는 viewModelScope를 사용합니다.

Pager 객체는 PagingSource 객체에서 load() 메서드를 호출하여 LoadParams 객체를 제공하고 반환되는 LoadResult 객체를 수신합니다.

RecyclerView 어댑터 정의

데이터를 RecyclerView 목록에 수신하는 어댑터도 설정해야 합니다. 페이징 라이브러리는 이러한 용도로 PagingDataAdapter 클래스를 제공합니다.

PagingDataAdapter를 확장하는 클래스를 정의합니다. 아래 예에서 UserAdapterPagingDataAdapter를 확장하고 유형 User의 목록 항목에 관해 UserViewHolder뷰 홀더로 사용하여 RecyclerView 어댑터를 제공합니다.

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 may be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item)
  }
}

자바

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 may be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item);
  }
}

자바

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 may be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item);
  }
}

또한 어댑터는 onCreateViewHolder()onBindViewHolder() 메서드를 정의하고 DiffUtil.ItemCallback을 지정해야 합니다. 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
  }
}

자바

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

자바

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

UI에 페이징된 데이터 표시

PagingSource를 정의하고 앱에서 PagingData 스트림을 생성하는 방법을 만들었으며 PagingDataAdapter를 정의했습니다. 이제 이러한 요소를 함께 연결하여 페이징된 데이터를 활동에 표시할 준비가 되었습니다.

활동의 onCreate 또는 프래그먼트의 onViewCreated 메서드에서 다음 단계를 진행하세요.

  1. PagingDataAdapter 클래스의 인스턴스를 만듭니다.
  2. 페이징된 데이터를 표시할 RecyclerView 목록에 PagingDataAdapter 인스턴스를 전달합니다.
  3. PagingData 스트림을 확인하고 생성된 각 값을 어댑터의 submitData() 메서드에 전달합니다.

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, but Fragments should instead use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  viewModel.flow.collectLatest { pagingData ->
    pagingAdapter.submitData(pagingData)
  }
}

자바

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

자바

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, but Fragments should instead use
// getViewLifecycleOwner().getLifecycle().
viewModel.liveData.observe(this, pagingData ->
  pagingAdapter.submitData(getLifecycle(), pagingData));

이제 RecyclerView 목록에 데이터 소스에서 페이징된 데이터가 표시되고 필요한 경우 자동으로 다른 페이지가 로드됩니다.

로드 상태 표시

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

  • 활성 로드 작업이 없고 오류가 없는 경우 LoadStateLoadState.NotLoading 객체입니다.
  • 활성 로드 작업이 있는 경우 LoadStateLoadState.Loading 객체입니다.
  • 오류가 있는 경우 LoadStateLoadState.Error 객체입니다.

UI에서 LoadState를 사용하는 데는 두 가지 방법이 있습니다. 즉, 리스너를 사용하는 방법과 특수 목록 어댑터를 사용하여 RecyclerView 목록에 로드 상태를 직접 표시하는 방법입니다.

리스너를 사용하여 로드 상태 가져오기

일반적인 용도로 UI에 로드 상태를 가져오기 위해 PagingDataAdapteraddLoadStateListener() 메서드가 포함되어 있습니다.

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

자바

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

자바

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

어댑터를 사용하여 로드 상태 표시

페이징 라이브러리는 표시된 페이징된 데이터 목록에 로드 상태를 직접 표시하는 용도로 LoadStateAdapter라는 또 다른 목록 어댑터를 제공합니다.

먼저 LoadStateAdapter를 구현하는 클래스를 만들고 onCreateViewHolder() 메서드와 onBindViewHolder() 메서드를 정의합니다.

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

// Adapter that displays a loading spinner when
// state = 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)
}

자바

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

// Adapter that displays a loading spinner when
// state instanceOf LoadState.Loading, and an error message and
// retry button when state instanceof 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);
  }
}

자바

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

// Adapter that displays a loading spinner when
// state instanceOf LoadState.Loading, and an error message and
// retry button when state instanceof 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)
  )

자바

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

자바

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

RecyclerView가 머리글이나 바닥글에만 로드 상태를 표시하게 하려면 withLoadStateHeader() 또는 withLoadStateFooter()를 대신 호출하면 됩니다.

추가 리소스

페이징 라이브러리에 관한 자세한 내용은 다음과 같은 추가 리소스를 참고하세요.

Codelab

샘플