Wczytywanie i wyświetlanie danych z podziałem na strony

Biblioteka stronowania zapewnia zaawansowane możliwości wczytywania i wyświetlania danych z większego zbioru danych. Z tego przewodnika dowiesz się, jak za pomocą biblioteki stronowania skonfigurować strumień danych stronowanych ze źródła danych w sieci i wyświetlić go w RecyclerView.

Definiowanie źródła danych

Pierwszym krokiem jest zdefiniowanie implementacji PagingSource, aby wskazać źródło danych. Klasa interfejsu API PagingSource zawiera metodę load(), którą możesz zastąpić, aby wskazać sposób pobierania danych podzielonych na strony z odpowiedniego źródła danych.

Aby używać coroutines Kotlin do ładowania asynchronicznego, korzystaj bezpośrednio z klasy PagingSource. Biblioteka Paging udostępnia też klasy do obsługi innych asynchronicznych frameworków:

Wybieranie typów kluczy i wartości

PagingSource<Key, Value> ma 2 parametry typu: KeyValue. Klucz definiuje identyfikator używany do wczytywania danych, a wartość to typ samych danych. Jeśli na przykład wczytujesz strony obiektów User z sieci, przekazując do funkcji Retrofit numery stron Int, wybierz typ Key jako Key i typ User jako Value.Int

Definiowanie PagingSource

W tym przykładzie implementujemy funkcję PagingSource, która wczytuje strony elementów według numeru strony. Typ Key to Int, a typ Value to 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 for
      // expected errors (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; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

Java

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

Java

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

Typowa implementacja klasy PagingSource przekazuje parametry podane w jej konstruktorze metodzie load(), aby załadować odpowiednie dane do zapytania. W przykładzie powyżej są to:

  • backend: instancja usługi backendowej, która dostarcza dane
  • query: wyszukiwane hasło do wysłania do usługi wskazanej przez backend

Obiekt LoadParams zawiera informacje o do wykonania operacji wczytania. Zawiera klucz do załadowania i liczbę elementów do załadowania.

Obiekt LoadResult zawiera wynik operacji wczytywania. LoadResult to zamknięta klasa, która może przybrać jedną z 2 form w zależności od tego, czy wywołanie load() zakończyło się powodzeniem:

  • Jeśli wczytanie się powiedzie, zwraca obiekt LoadResult.Page.
  • Jeśli wczytanie nie powiedzie się, zwracany jest obiekt LoadResult.Error.

Na rysunku poniżej widać, jak funkcja load() w tym przykładzie otrzymuje klucz dla każdego wczytania i przekazuje klucz dla kolejnego wczytania.

W przypadku każdego wywołania load() metoda ExamplePagingSource pobiera bieżący klucz
    i zwraca kolejny klucz do załadowania.
Rysunek 1. Schemat pokazujący, jak usługa load() używa i aktualizuje klucz.

Implementacja PagingSource musi też zawierać metodę getRefreshKey(), która przyjmuje jako parametr obiekt PagingState. Zwraca klucz, który należy przekazać metodzie load(), gdy dane zostaną odświeżone lub unieważnione po początkowym załadowaniu. Biblioteka stron wywołuje tę metodę automatycznie podczas kolejnych odświeżeń danych.

Obsługa błędów

Żądania wczytania danych mogą się nie udać z różnych powodów, zwłaszcza podczas wczytywania przez sieć. Zgłaszaj błędy występujące podczas wczytywania, zwracając obiekt LoadResult.Error z metody load().

Możesz na przykład wykrywać i zgłaszać błędy wczytywania w metodie ExamplePagingSource z poprzedniego przykładu, dodając do niej ten kod:load()

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

Java

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

Java

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

Więcej informacji o rozwiązywaniu błędów w Retrofit znajdziesz w przykładach w PagingSource dokumentacji interfejsu API.

PagingSource zbiera i przekazuje obiekty LoadResult.Error do interfejsu, aby umożliwić Ci na nich działanie. Więcej informacji o wyświetlaniu stanu wczytywania w interfejsie użytkownika znajdziesz w artykule Zarządzanie stanami wczytywania i ich prezentowanie.

Konfigurowanie strumienia PagingData

Następnie musisz mieć strumień danych z tabulacją z implementacji PagingSource. Skonfiguruj strumień danych w ViewModel. Klasa Pager udostępnia metody, które udostępniają reaktywny strumień obiektów PagingData z poziomu PagingSource. Biblioteka Paging obsługuje kilka typów strumieni, w tym Flow, LiveData oraz typy FlowableObservable z RxJava.

Podczas tworzenia wystąpienia Pager, aby skonfigurować strumień reaktywny, musisz przekazać mu obiekt konfiguracji PagingConfig i funkcję, która informuje Pager, jak uzyskać wystąpienie implementacji PagingSource:

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)

Java

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

Java

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

Operator cachedIn() udostępnia strumień danych i zapisują w pamięci podręcznej załadowane dane za pomocą udostępnionego CoroutineScope. W tym przykładzie użyto artefaktu viewModelScope udostępnionego przez artefakt lifecycle-viewmodel-ktx cyklu życia.

Obiekt Pager wywołuje metodę load() obiektu PagingSource, przekazując mu obiekt LoadParams i otrzymując w zamian obiekt LoadResult.

Definiowanie adaptera RecyclerView

Musisz też skonfigurować adapter, aby dane były odbierane na liście RecyclerView. Biblioteka stron zawiera klasę PagingDataAdapter, która służy do tego celu.

Określ klasę rozszerzającą PagingDataAdapter. W tym przykładzie klasa UserAdapter rozszerza klasę PagingDataAdapter, aby udostępnić adapter RecyclerView dla elementów listy typu User, używając klasy UserViewHolder jako holdera widoku:

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

Java

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

Java

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

Adapter musi też definiować metody onCreateViewHolder()onBindViewHolder() oraz określać parametr DiffUtil.ItemCallback. Działa to tak samo jak podczas definiowania adapterów listy 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
  }
}

Java

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

Java

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

Wyświetlanie danych po stronie w interfejsie użytkownika

Po zdefiniowaniu PagingSource, utworzenia sposobu generowania przez aplikację strumienia PagingData oraz zdefiniowaniu PagingDataAdapter możesz połączyć te elementy i wyświetlać dane podzielone na strony w swojej aktywności.

W metodzie onCreate aktywności lub onViewCreated fragmentu wykonaj te czynności:

  1. Utwórz instancję klasy PagingDataAdapter.
  2. Przekaż instancję PagingDataAdapter do listy RecyclerView, na której chcesz wyświetlać dane podzielone na strony.
  3. Obserwuj strumień PagingData i przekazuj każdą wygenerowaną wartość do metody submitData() w adapterze.

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; fragments use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  viewModel.flow.collectLatest { pagingData ->
    pagingAdapter.submitData(pagingData)
  }
}

Java

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

Java

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

Lista RecyclerView wyświetla teraz dane z każdej strony źródła danych i w razie potrzeby automatycznie wczytuje kolejną stronę.

Dodatkowe materiały

Więcej informacji o bibliotece Paging znajdziesz w tych materiałach:

Ćwiczenia z programowania