Konzepte und Jetpack Compose-Implementierung
Die Paging Library bietet leistungsstarke Funktionen zum Laden und Anzeigen von Seiten mit Daten aus einem größeren Dataset. In dieser Anleitung wird gezeigt, wie Sie mit der Paging
Library einen Stream mit Seiten mit 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 PagingSource API-Klasse enthält die load-Methode,
die Sie überschreiben, um anzugeben, wie Seiten mit 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 Library bietet auch Klassen zur Unterstützung anderer asynchroner Frameworks:
- Wenn Sie RxJava verwenden möchten, implementieren Sie
RxPagingSourcestattdessen. - Wenn Sie
ListenableFutureaus Guava verwenden möchten, implementieren SieListenableFuturePagingSourcestattdessen.
Schlüssel- und Werttypen auswählen
PagingSource<Key, Value> hat zwei Typparameter: Key und Value. Der Schlüssel definiert die ID, 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 den
Key-Typ und User als den Value-Typ aus.
PagingSource definieren
Im folgenden Beispiel wird eine PagingSource implementiert, die Seiten mit Elementen lädt
nach Seitenzahl. Der Key-Typ ist Int und der Value-Typ 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;
}
}
Eine typische `PagingSource`-Implementierung übergibt Parameter, die im Konstruktor angegeben wurden, an die Methode load, um die entsprechenden Daten für eine Abfrage zu laden. Im obigen Beispiel sind das folgende Parameter:
backend: eine Instanz des Backend-Dienstes, der die Daten bereitstelltquery: die Suchanfrage, die an den durchbackendangegebenen Dienst gesendet werden soll
Das LoadParams Objekt enthält Informationen zum auszuführenden Ladevorgang. Dazu gehören der zu ladende Schlüssel und die Anzahl der zu ladenden Elemente.
Das LoadResult Objekt 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 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.
load den Schlüssel verwendet und aktualisiert.
Die PagingSource Implementierung muss auch eine getRefreshKey
Methode implementieren, die ein PagingState Objekt als Parameter akzeptiert. Sie 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 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 von der 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 Fehlerbehandlung in Retrofit finden Sie in den Beispielen in der API-Referenz zu PagingSource.
PagingSource erfasst LoadResult.Error-Objekte und stellt sie der Benutzeroberfläche zur Verfügung, damit Sie darauf reagieren können. Weitere Informationen zum Bereitstellen des Ladestatus auf
der Benutzeroberfläche finden Sie unter Ladestatus verwalten und präsentieren.
Stream von PagingData einrichten
Als Nächstes benötigen Sie einen Stream mit Seiten mit Daten aus der PagingSource-Implementierung.
Richten Sie den Datenstream in Ihrem ViewModel ein. Die Pager Klasse bietet
Methoden, die einen reaktiven Stream von PagingData Objekten aus einer
PagingSource bereitstellen. Die Paging Library unterstützt die Verwendung verschiedener Streamtypen,
darunter Flow, LiveData, und 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 werden kann:
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 load Methode aus dem PagingSource Objekt auf,
stellt ihm das LoadParams Objekt zur Verfügung und empfängt im Gegenzug das
LoadResult Objekt.
RecyclerView-Adapter definieren
Sie müssen auch einen Adapter einrichten, um die Daten in Ihrer RecyclerView-Liste zu empfangen. Die Paging Library bietet dafür die Klasse PagingDataAdapter.
Definieren Sie eine Klasse, die PagingDataAdapter erweitert. Im Beispiel erweitert UserAdapter
PagingDataAdapter, um einen RecyclerView-Adapter für Listenelemente
vom Typ User bereitzustellen, wobei UserViewHolder als View-Holder verwendet wird:
Kotlin (Coroutinen)
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 onCreateViewHolder und
onBindViewHolder Methoden definieren und ein DiffUtil.ItemCallback angeben. Das funktioniert genauso wie beim Definieren von RecyclerView-Listenadaptern:
Kotlin (Coroutinen)
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);
}
}
Seiten mit Daten auf der Benutzeroberfläche anzeigen
Nachdem Sie eine PagingSource definiert, eine Möglichkeit für Ihre App zum
Generieren eines Streams von PagingData erstellt und einen PagingDataAdapter definiert haben, können Sie diese Elemente miteinander verbinden und Seiten mit Daten in Ihrer
Aktivität anzeigen.
Führen Sie in der Methode onCreate Ihrer Aktivität oder 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 Sie die Seiten mit Daten anzeigen möchten. - Beobachten Sie den
PagingData-Stream und übergeben Sie jeden generierten Wert an die MethodesubmitData()Ihres Adapters.
Kotlin (Coroutinen)
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 RecyclerView-Liste werden jetzt die Seiten mit Daten aus der Datenquelle angezeigt und bei Bedarf automatisch eine weitere Seite geladen.