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:
- Wenn Sie RxJava verwenden möchten, implementieren Sie stattdessen
RxPagingSource
. - Wenn Sie
ListenableFuture
von Guava verwenden möchten, implementieren Sie stattdessenListenableFuturePagingSource
.
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 bereitstelltquery
: Die Suchanfrage, die an den mitbackend
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.
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:
- Erstellen Sie eine Instanz der Klasse
PagingDataAdapter
. - Übergeben Sie die
PagingDataAdapter
-Instanz an die ListeRecyclerView
, in der die gepufferten Daten angezeigt werden sollen. - Beobachte den
PagingData
-Stream und übergebe jeden generierten Wert an diesubmitData()
-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
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Seite aus Netzwerk und Datenbank
- Zu Paging 3 migrieren
- Übersicht über die Auslagerungsbibliothek