Seitendaten laden und anzeigen

Die Paginierungsbibliothek bietet leistungsstarke Funktionen zum Laden und Anzeigen von Auslagerungsdaten aus einem größeren Dataset. In diesem Leitfaden wird beschrieben, wie Sie mit der Paging-Bibliothek einen Stream mit Seitendaten aus einer Netzwerkdatenquelle einrichten und in einem RecyclerView darstellen.

Datenquelle definieren

Im ersten Schritt definieren Sie eine PagingSource-Implementierung, um die Datenquelle zu identifizieren. Die API-Klasse PagingSource enthält die Methode load(), die Sie überschreiben, um anzugeben, wie Seitendaten aus der entsprechenden Datenquelle abgerufen werden.

Verwenden Sie die Klasse PagingSource direkt, um Kotlin-Koroutinen für asynchrones Laden zu nutzen. Die Paging-Bibliothek bietet auch Klassen zur Unterstützung anderer asynchroner Frameworks:

Schlüssel- und Werttypen auswählen

PagingSource<Key, Value> hat zwei Typparameter: Key und Value. Der Schlüssel definiert die Kennung, die zum Laden der Daten verwendet wird, und der Wert ist der Typ der Daten selbst. Wenn Sie beispielsweise Seiten mit User-Objekten aus dem Netzwerk laden möchten, indem Sie Int-Seitennummern an Retrofit übergeben, wählen Sie Int als Typ Key und User als Typ Value aus.

PagingSource definieren

Im folgenden Beispiel wird ein PagingSource implementiert, mit dem Seiten mit Elementen nach Seitennummer geladen werden. Der Typ Key ist Int und der Typ Value ist 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;
  }
}

Eine typische PagingSource-Implementierung übergibt die in ihrem Konstruktor bereitgestellten Parameter an die Methode load(), um geeignete Daten für eine Abfrage zu laden. Im Beispiel oben sind diese Parameter:

  • backend: eine Instanz des Back-End-Dienstes, der die Daten bereitstellt
  • query: Suchanfrage, die an den durch backend angegebenen Dienst gesendet werden soll

Das Objekt LoadParams enthält Informationen zum auszuführenden Ladevorgang. Dazu gehören der zu ladende Schlüssel und die Anzahl der zu ladenden Elemente.

Das Objekt LoadResult enthält das Ergebnis des Ladevorgangs. LoadResult ist eine versiegelte Klasse, die je nachdem, ob der load()-Aufruf erfolgreich war, eine von zwei Formen annehmen kann:

  • Wenn der Ladevorgang erfolgreich ist, geben Sie ein LoadResult.Page-Objekt zurück.
  • Wenn der Ladevorgang nicht erfolgreich ist, geben Sie ein LoadResult.Error-Objekt zurück.

Die folgende Abbildung zeigt, wie die Funktion load() in diesem Beispiel den Schlüssel für jeden Ladevorgang empfängt und den Schlüssel für den nachfolgenden Ladevorgang bereitstellt.

Bei jedem „load()“-Aufruf übernimmt das ExamplePagingSource den aktuellen Schlüssel und gibt den nächsten Schlüssel zum Laden zurück.
Abbildung 1. Diagramm, das zeigt, wie load() den Schlüssel verwendet und aktualisiert.

In der PagingSource-Implementierung muss außerdem eine Methode getRefreshKey() implementiert werden, die ein PagingState-Objekt als Parameter verwendet. Sie gibt den Schlüssel zurück, der an die Methode load() übergeben wird, wenn die Daten nach dem anfänglichen Ladevorgang aktualisiert oder entwertet werden. Die Paging Library ruft diese Methode bei nachfolgenden Aktualisierungen der Daten automatisch auf.

Fehler beheben

Anfragen zum Laden von Daten können aus verschiedenen Gründen fehlschlagen, insbesondere beim Laden über ein Netzwerk. Beim Laden aufgetretene Fehler durch Rückgabe eines LoadResult.Error-Objekts aus der Methode load() melden.

Sie können beispielsweise Ladefehler in ExamplePagingSource aus dem vorherigen Beispiel erfassen und melden, indem Sie der Methode load() Folgendes hinzufügen:

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

Weitere Informationen zum Umgang mit Retrofit-Fehlern finden Sie in den Beispielen in der PagingSource-API-Referenz.

PagingSource erfasst und übergibt LoadResult.Error-Objekte an die UI, damit Sie darauf reagieren können. Weitere Informationen zum Bereitstellen des Ladestatus in der UI finden Sie unter Ladestatus verwalten und präsentieren.

Stream von PagingData einrichten

Als Nächstes benötigen Sie einen Stream mit Seitendaten aus der PagingSource-Implementierung. Richten Sie den Datenstream in der ViewModel ein. Die Klasse Pager bietet Methoden, die einen reaktiven Stream von PagingData-Objekten aus einer PagingSource freigeben. Die Paginierungsbibliothek unterstützt die Verwendung mehrerer Streamtypen, darunter Flow, LiveData sowie die Typen Flowable und Observable von RxJava.

Wenn Sie eine Pager-Instanz zum Einrichten eines reaktiven Streams erstellen, müssen Sie der Instanz ein PagingConfig-Konfigurationsobjekt und eine Funktion zur Verfügung stellen, die Pager mitteilt, wie eine Instanz Ihrer PagingSource-Implementierung abgerufen wird:

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

Mit dem Operator cachedIn() kann der Datenstream geteilt werden und die geladenen Daten werden mit dem bereitgestellten CoroutineScope im Cache gespeichert. In diesem Beispiel wird die vom Lebenszyklus lifecycle-viewmodel-ktx-Artefakt bereitgestellte viewModelScope verwendet.

Das Pager-Objekt ruft die Methode load() aus dem PagingSource-Objekt auf, stellt es mit dem LoadParams-Objekt bereit und empfängt als Antwort das LoadResult-Objekt.

RecyclerView-Adapter definieren

Außerdem müssen Sie einen Adapter einrichten, um die Daten in die RecyclerView-Liste aufzunehmen. Die Paginierungsbibliothek stellt zu diesem Zweck die Klasse PagingDataAdapter bereit.

Definieren Sie eine Klasse, die PagingDataAdapter erweitert. Im Beispiel erweitert UserAdapter PagingDataAdapter, um einen RecyclerView-Adapter für Listenelemente vom Typ User bereitzustellen und UserViewHolder als Ansichtsinhaber zu verwenden:

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

Der Adapter muss auch die Methoden onCreateViewHolder() und onBindViewHolder() definieren und einen DiffUtil.ItemCallback angeben. Das funktioniert genauso wie beim Definieren von RecyclerView-Listenadaptern:

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

Seitendaten auf Ihrer Benutzeroberfläche anzeigen

Nachdem Sie ein PagingSource definiert, eine Methode zum Generieren eines PagingData-Streams durch Ihre App und ein PagingDataAdapter definiert haben, können Sie diese Elemente verbinden und in Ihrer Aktivität seitenbasierte Daten anzeigen.

Führe die folgenden Schritte in der Methode onCreate oder der Methode onViewCreated des Fragments aus:

  1. Erstellen Sie eine Instanz Ihrer PagingDataAdapter-Klasse.
  2. Übergeben Sie die Instanz PagingDataAdapter an die Liste RecyclerView, in der die seitenbasierten Daten angezeigt werden sollen.
  3. Beobachten Sie den PagingData-Stream und übergeben Sie jeden generierten Wert an die submitData()-Methode des Adapters.

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

In der Liste RecyclerView werden jetzt die Daten aus der Datenquelle angezeigt. Bei Bedarf wird automatisch eine weitere Seite geladen.

Weitere Informationen

Weitere Informationen zur Paging-Bibliothek finden Sie in den folgenden Ressourcen:

Codelabs

Produktproben