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:
- Wenn Sie RxJava verwenden möchten, implementieren Sie stattdessen
RxPagingSource. - Wenn Sie
ListenableFutureaus Guava verwenden möchten, implementieren Sie stattdessenListenableFuturePagingSource.
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 durchbackendangegeben 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.
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:
- Erstellen Sie eine Instanz Ihrer
PagingDataAdapter-Klasse. - Übergeben Sie die
PagingDataAdapter-Instanz an dieRecyclerView-Liste, in der die paginierten Daten angezeigt werden sollen. - Beobachten Sie den
PagingData-Stream und übergeben Sie jeden generierten Wert an diesubmitData()-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.