Seitendaten laden und anzeigen

Die Paging-Bibliothek bietet leistungsstarke Funktionen zum Laden und Darstellen von paginierten Daten aus einem größeren Datenpool. In dieser Anleitung wird gezeigt, wie Sie mit der Paging-Bibliothek einen Stream mit paginaten Daten aus einer Netzwerkdatenquelle einrichten und in einer RecyclerView anzeigen.

Datenquelle definieren

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

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

Schlüssel- und Wertetypen auswählen

PagingSource<Key, Value> hat zwei Typparameter: Key und Value. Der Schlüssel definiert die Kennung, mit der die Daten geladen werden, und der Wert ist der Datentyp selbst. Wenn du beispielsweise Seiten mit User-Objekten aus dem Netzwerk lädst, indem du Int-Seitennummern an Retrofit übergibst, wähle Int als Key-Typ und User als Value-Typ aus.

PagingSource definieren

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

Bei einer typischen PagingSource-Implementierung werden die im Konstruktor bereitgestellten Parameter an die load()-Methode übergeben, um die entsprechenden Daten für eine Abfrage zu laden. Im obigen Beispiel sind das:

  • backend: eine Instanz des Backend-Dienstes, der die Daten bereitstellt
  • query: Die Suchanfrage, die an den mit backend angegebenen Dienst gesendet werden soll.

Das Objekt LoadParams enthält Informationen zum durchzufü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 die Daten geladen werden konnten, gib ein LoadResult.Page-Objekt zurück.
  • Wenn die Daten nicht geladen werden können, gib ein LoadResult.Error-Objekt zurück.

Die folgende Abbildung veranschaulicht, wie die load()-Funktion in diesem Beispiel den Schlüssel für jede Ladung empfängt und den Schlüssel für die nachfolgende Ladung bereitstellt.

Bei jedem Aufruf von „load()“ nimmt die Beispiel-Paging-Quelle den aktuellen Schlüssel entgegen und gibt den nächsten Schlüssel zurück, der geladen werden soll.
Abbildung 1: Diagramm, das zeigt, wie load() den Schlüssel verwendet und aktualisiert

Die PagingSource-Implementierung muss außerdem eine getRefreshKey()-Methode implementieren, die ein PagingState-Objekt als Parameter annimmt. Diese Funktion gibt den Schlüssel zurück, der an die load()-Methode übergeben wird, wenn die Daten nach dem ersten Laden aktualisiert oder ungültig gemacht werden. Die Paging-Bibliothek ruft diese Methode bei nachfolgenden Aktualisierungen der Daten automatisch auf.

Fehler verarbeiten

Anfragen zum Laden von Daten können aus verschiedenen Gründen fehlschlagen, insbesondere beim Laden über ein Netzwerk. Melden Sie Fehler, die beim Laden auftreten, indem Sie ein LoadResult.Error-Objekt aus der load()-Methode zurückgeben.

So können Sie beispielsweise Ladefehler in ExamplePagingSource aus dem vorherigen Beispiel abfangen 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 zur Behandlung von Retrofit-Fehlern finden Sie in den Beispielen in der PagingSource API-Referenz.

PagingSource erfasst und übergibt LoadResult.Error-Objekte an die Benutzeroberfläche, damit Sie sie bearbeiten können. Weitere Informationen zum Anzeigen des Ladevorgangs in der Benutzeroberfläche finden Sie unter Ladevorgänge verwalten und präsentieren.

Stream mit PagingData einrichten

Als Nächstes benötigen Sie einen Stream mit paginaten Daten aus der PagingSource-Implementierung. Richten Sie den Datenstream in Ihrer ViewModel ein. Die Klasse Pager bietet Methoden, die einen reaktiven Stream von PagingData-Objekten aus einem PagingSource bereitstellen. Die Paging-Bibliothek unterstützt mehrere Streamtypen, darunter Flow, LiveData sowie die Typen Flowable und Observable aus RxJava.

Wenn du eine Pager-Instanz erstellst, um deinen reaktiven Stream einzurichten, musst du der Instanz ein PagingConfig-Konfigurationsobjekt und eine Funktion zur Verfügung stellen, die Pager angibt, wie eine Instanz deiner PagingSource-Implementierung abgerufen werden kann:

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() können Sie den Datenstream freigeben und die geladenen Daten mit dem angegebenen CoroutineScope im Cache speichern. In diesem Beispiel wird der viewModelScope verwendet, der vom Lebenszyklus-Artefakt lifecycle-viewmodel-ktx bereitgestellt wird.

Das Pager-Objekt ruft die load()-Methode des PagingSource-Objekts auf, wobei es das LoadParams-Objekt übergibt und im Gegenzug das LoadResult-Objekt erhält.

RecyclerView-Adapter definieren

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

Definieren Sie eine Klasse, die PagingDataAdapter erweitert. In diesem Beispiel wird UserAdapter durch PagingDataAdapter erweitert, um einen RecyclerView-Adapter für Listenelemente vom Typ User bereitzustellen und UserViewHolder als Ansichtshalter 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 außerdem die Methoden onCreateViewHolder() und onBindViewHolder() definieren und einen DiffUtil.ItemCallback angeben. Das funktioniert genauso wie bei der Definition 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);
  }
}

Die gepufferten Daten in der Benutzeroberfläche anzeigen

Nachdem Sie eine PagingSource definiert, eine Möglichkeit für Ihre App zum Generieren eines PagingData-Streams erstellt und eine PagingDataAdapter definiert haben, können Sie diese Elemente verbinden und gegliederte Daten in Ihrer Aktivität anzeigen.

Führen Sie in der onCreate-Methode Ihrer Aktivität oder der onViewCreated-Methode Ihres Fragments die folgenden Schritte aus:

  1. Erstellen Sie eine Instanz der Klasse PagingDataAdapter.
  2. Übergeben Sie die PagingDataAdapter-Instanz an die Liste RecyclerView, in der die gepufferten Daten angezeigt werden sollen.
  3. Beobachte den PagingData-Stream und übergebe jeden generierten Wert an die submitData()-Methode deines 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 paginaten Daten aus der Datenquelle angezeigt und bei Bedarf wird automatisch eine weitere Seite geladen.

Weitere Informationen

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

Codelabs