Paginierte Daten laden und anzeigen (Ansichten)

Konzepte und Jetpack Compose-Implementierung

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

Datenquelle definieren

Als Erstes müssen Sie eine PagingSource-Implementierung definieren, um die Datenquelle zu identifizieren. Die API-Klasse PagingSource enthält die Methode load, die Sie überschreiben, um anzugeben, wie paginierte Daten aus der entsprechenden Datenquelle abgerufen werden.

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 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, indem Sie Int-Seitenzahlen an Retrofit übergeben, wählen Sie Int als Key-Typ und User als Value-Typ aus.

PagingSource definieren

Im folgenden Beispiel wird ein PagingSource implementiert, das Seiten mit Elementen anhand der Seitenzahl lädt. Der Typ Key ist Int und der Typ Value ist User.

Java (RxJava)

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 (Guava/LiveData)

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 Methode load ü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 Dienst gesendet werden soll, der durch backend angegeben wird.

Das Objekt LoadParams enthält Informationen zum Ladevorgang, der ausgeführt werden soll. 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 das Laden erfolgreich ist, geben Sie ein LoadResult.Page-Objekt zurück.
  • Wenn das Laden nicht erfolgreich ist, geben Sie ein LoadResult.Error-Objekt zurück.

Die folgende Abbildung veranschaulicht, 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 Ladeaufruf verwendet die ExamplePagingSource den aktuellen Schlüssel und gibt den nächsten zu ladenden Schlüssel zurück.
Abbildung 1: Diagramm, das zeigt, wie load den Schlüssel verwendet und aktualisiert.

Die PagingSource-Implementierung muss auch eine getRefreshKey-Methode implementieren, die ein PagingState-Objekt als Parameter verwendet. Er gibt den Schlüssel zurück, der an die Methode load übergeben werden soll, wenn die Daten nach dem ersten Laden aktualisiert oder ungültig gemacht werden. Die Paging Library ruft diese Methode automatisch bei nachfolgenden Aktualisierungen der Daten 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 über die Methode load zurückgeben.

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

Java (RxJava)

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

Java (Guava/LiveData)

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 stellt LoadResult.Error-Objekte für die Benutzeroberfläche bereit, damit Sie darauf reagieren können. Weitere Informationen zum Einblenden des Ladestatus in der Benutzeroberfläche finden Sie unter Ladestatus verwalten und präsentieren.

Stream von PagingData einrichten

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

Wenn Sie eine Pager-Instanz erstellen, um Ihren reaktiven Stream einzurichten, müssen Sie der Instanz ein PagingConfig-Konfigurationsobjekt und eine Funktion bereitstellen, die Pager mitteilt, wie eine Instanz Ihrer PagingSource-Implementierung abgerufen wird:

Java (RxJava)

// 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 (Guava/LiveData)

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

Der Operator cachedIn macht den Datenstream freigabefähig und speichert die geladenen Daten mit dem bereitgestellten CoroutineScope im Cache. In diesem Beispiel wird viewModelScope verwendet, das vom Lifecycle-Artefakt lifecycle-viewmodel-ktx bereitgestellt wird.

Das Pager-Objekt ruft die Methode load aus dem PagingSource-Objekt auf, stellt ihr das LoadParams-Objekt zur Verfügung und empfängt im Gegenzug das LoadResult-Objekt.

RecyclerView-Adapter definieren

Außerdem müssen Sie einen Adapter einrichten, damit die Daten in Ihre RecyclerView-Liste aufgenommen werden. Die Paging-Bibliothek stellt dafür die Klasse PagingDataAdapter bereit.

Definieren Sie eine Klasse, die PagingDataAdapter erweitert. Im Beispiel wird UserAdapter von PagingDataAdapter abgeleitet, um einen RecyclerView-Adapter für Listenelemente vom Typ User bereitzustellen, der UserViewHolder als View-Holder verwendet:

Kotlin (Koroutinen)

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 (RxJava)

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 (Guava/LiveData)

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

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

Kotlin (Koroutinen)

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 (RxJava)

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 (Guava/LiveData)

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

Paginierte Daten in der Benutzeroberfläche anzeigen

Nachdem Sie nun eine PagingSource definiert, eine Möglichkeit für Ihre App geschaffen haben, einen Stream von PagingData zu generieren, und eine PagingDataAdapter definiert haben, können Sie diese Elemente miteinander verbinden und die paginierten Daten in Ihrer Aktivität anzeigen.

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

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

Kotlin (Koroutinen)

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 (RxJava)

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 (Guava/LiveData)

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 paginierten Daten aus der Datenquelle angezeigt und bei Bedarf wird automatisch eine weitere Seite geladen.