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

Biblioteka stronnicza zapewnia zaawansowane możliwości wczytywania i wyświetlania z większego zbioru danych. Ten przewodnik pokazuje, jak korzystać ze stronicowania do konfigurowania strumienia danych stronicowanych z sieciowego źródła danych go w RecyclerView.

Zdefiniuj źródło danych

Pierwszym krokiem jest zdefiniowanie Wdrożenie PagingSource do zidentyfikowania źródła danych. Klasa interfejsu API PagingSource zawiera load(). , którą zmieniasz, aby wskazać sposób pobierania danych stronicowanych z odpowiadającemu źródłu danych.

Używaj bezpośrednio klasy PagingSource do używania współrzędnych Kotlina na potrzeby asynchronicznego wczytuję. Biblioteka stronnicza zawiera też klasy obsługujące inne asynchroniczne platformy:

.

Wybierz typy kluczy i wartości

Funkcja PagingSource<Key, Value> ma 2 parametry typu: Key i Value. Klucz określa identyfikator używany do wczytywania danych, a wartością jest typ danych. Jeśli na przykład wczytujesz strony obiektów User z sieci przekazując numery stron (Int) do Retrofit, wybierz Int jako typ Key i User jako typ Value.

Definiowanie źródła strony

W poniższym przykładzie zastosowano model PagingSource, który się wczytuje stron 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 PagingSource przekazuje parametry podane w do metody load(), aby wczytać dane odpowiednie dla zapytania. W te parametry to:

  • backend: instancja usługi backendu, która dostarcza dane
  • query: zapytanie, które ma zostać wysłane do usługi wskazywanej przez parametr backend.

LoadParams zawiera informacje o operacji wczytywania, która ma zostać wykonana. Ten zawiera klucz do załadowania oraz liczbę elementów do załadowania.

LoadResult zawiera wynik operacji wczytywania. LoadResult to zapleczona klasa który ma jedną z dwóch form, w zależności od tego, czy wywołanie load() zakończyło się sukcesem:

  • Jeśli wczytywanie się uda, zwróć obiekt LoadResult.Page.
  • Jeśli wczytywanie się nie powiedzie, zwróć obiekt LoadResult.Error.

Poniższy rysunek pokazuje, jak funkcja load() w tym przykładzie odbiera klucz przy każdym obciążeniu i dostarcza go na potrzeby kolejnego wczytywania.

Przy każdym wywołaniu load() narzędzie ExamplePagingSource przyjmuje bieżący klucz.
    i zwraca następny klucz do wczytania.
Rysunek 1. Diagram pokazujący, jak load() wykorzystuje i aktualizuje klucz.

Implementacja PagingSource musi też implementować getRefreshKey(). która pobiera PagingState jako . Zwraca klucz przekazywany do metody load(), gdy dane są odświeżony lub unieważniony po początkowym wczytaniu. Biblioteka stron internetowych nazywa tę funkcję przy kolejnych odświeżeniach danych.

Obsługa błędów

Żądania wczytania danych mogą zakończyć się niepowodzeniem z różnych powodów, zwłaszcza podczas wczytywania w sieci. Zgłoś błędy napotkane podczas wczytywania przez zwrócenie Obiekt LoadResult.Error z metody load().

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

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 postępowaniu w przypadku błędów Retrofit znajdziesz w przykładach w Dokumentacja API PagingSource.

PagingSource zbiera i dostarcza LoadResult.Error obiekty do UI, co możesz zrobić. Więcej informacji o ujawnianiu stanu wczytywania w interfejsie, przeczytaj artykuł Zarządzanie wczytywaniem i prezentowanie go .

Konfigurowanie strumienia PagingData

Następnie potrzebny będzie strumień danych z podziałem na strony z implementacji PagingSource. Skonfiguruj strumień danych w narzędziu ViewModel. Klasa Pager udostępnia metody, które ujawnimy w reakcji strumień PagingData obiektów z PagingSource Biblioteka stron docelowych obsługuje kilka typów strumieni: w tym Flow, LiveData oraz typy Flowable i Observable z RxJava.

Gdy utworzysz instancję Pager, aby skonfigurować strumień reaktywny, musisz udostępnia instancji Konfiguracja PagingConfig i funkcję, która informuje Pager, jak pobrać wystąpienie Twojego Implementacja 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() umożliwia udostępnianie strumienia danych i zapisuje wczytywany strumień w pamięci podręcznej za pomocą podanej metody CoroutineScope. W tym przykładzie użyto atrybutu viewModelScope dostarczone przez artefakt cyklu życia lifecycle-viewmodel-ktx.

Obiekt Pager wywołuje metodę load() z obiektu PagingSource, i udostępnianiu LoadParams obiekt i otrzymujesz Obiekt LoadResult w zamian.

Definiowanie adaptera RecyclerView

Musisz też skonfigurować adapter, aby odbierać dane na urządzeniu RecyclerView z listy. Biblioteka stron internetowych udostępnia klasę PagingDataAdapter do tego celu cel.

Zdefiniuj klasę, która rozszerza zakres PagingDataAdapter. W tym przykładzie UserAdapter wydłuża okres PagingDataAdapter o RecyclerView adapter dla elementów listy typu User i korzystania z UserViewHolder jako widoku posiadacz:

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

Przejściówka musi również określać onCreateViewHolder() i onBindViewHolder() i określ DiffUtil.ItemCallback. Działa to tak samo jak podczas definiowania listy RecyclerView adaptery:

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 stronicowanych w interfejsie

Zdefiniowano już PagingSource, a teraz aplikacja może a generowany jest strumień o wartości PagingData i definiowany jest PagingDataAdapter, jesteś aby połączyć te elementy i wyświetlić dane stron działania.

Wykonaj te czynności w onCreate lub we fragmencie aktywności Metoda onViewCreated:

  1. Utwórz instancję klasy PagingDataAdapter.
  2. Przekaż instancję PagingDataAdapter do RecyclerView. na której mają być wyświetlane dane stronicowane.
  3. Obserwuj strumień PagingData i przekazuj każdą wygenerowaną wartość do swojego metody submitData() adaptera.

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

Na liście RecyclerView są teraz wyświetlane dane stronicowane ze źródła danych oraz w razie potrzeby automatycznie wczytuje inną stronę.

Dodatkowe materiały

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

Ćwiczenia z programowania

Próbki

. .