Die Paginierungsbibliothek bietet leistungsstarke Funktionen zum Laden und Anzeigen von Auslagerungsdaten aus einem größeren Dataset. In diesem Leitfaden wird beschrieben, wie Sie mit der Paging-Bibliothek einen Stream mit Seitendaten aus einer Netzwerkdatenquelle einrichten und in einem RecyclerView
darstellen.
Datenquelle definieren
Im ersten Schritt definieren Sie eine PagingSource
-Implementierung, um die Datenquelle zu identifizieren. Die API-Klasse PagingSource
enthält die Methode load()
, die Sie überschreiben, um anzugeben, wie Seitendaten aus der entsprechenden Datenquelle abgerufen werden.
Verwenden Sie die Klasse PagingSource
direkt, um Kotlin-Koroutinen für asynchrones Laden zu nutzen. Die Paging-Bibliothek bietet auch Klassen zur Unterstützung anderer asynchroner Frameworks:
- Wenn Sie RxJava verwenden möchten, müssen Sie stattdessen
RxPagingSource
implementieren. - Wenn Sie
ListenableFuture
von 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 möchten, indem Sie Int
-Seitennummern an Retrofit übergeben, wählen Sie Int
als Typ Key
und User
als Typ Value
aus.
PagingSource definieren
Im folgenden Beispiel wird ein PagingSource
implementiert, mit dem Seiten mit Elementen nach Seitennummer geladen werden. Der Typ Key
ist Int
und der Typ Value
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; } }
Eine typische PagingSource
-Implementierung übergibt die in ihrem Konstruktor bereitgestellten Parameter an die Methode load()
, um geeignete Daten für eine Abfrage zu laden. Im Beispiel oben sind diese Parameter:
backend
: eine Instanz des Back-End-Dienstes, der die Daten bereitstelltquery
: Suchanfrage, die an den durchbackend
angegebenen Dienst gesendet werden soll
Das Objekt LoadParams
enthält Informationen zum auszufü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 der Ladevorgang erfolgreich ist, geben Sie ein
LoadResult.Page
-Objekt zurück. - Wenn der Ladevorgang 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.
In der PagingSource
-Implementierung muss außerdem eine Methode getRefreshKey()
implementiert werden, die ein PagingState
-Objekt als Parameter verwendet. Sie gibt den Schlüssel zurück, der an die Methode load()
übergeben wird, wenn die Daten nach dem anfänglichen Ladevorgang aktualisiert oder entwertet werden. Die Paging Library ruft diese Methode bei nachfolgenden Aktualisierungen der Daten automatisch auf.
Fehler beheben
Anfragen zum Laden von Daten können aus verschiedenen Gründen fehlschlagen, insbesondere beim Laden über ein Netzwerk. Beim Laden aufgetretene Fehler durch Rückgabe eines LoadResult.Error
-Objekts aus der Methode load()
melden.
Sie können beispielsweise Ladefehler in ExamplePagingSource
aus dem vorherigen Beispiel erfassen 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 zum Umgang mit Retrofit-Fehlern finden Sie in den Beispielen in der PagingSource
-API-Referenz.
PagingSource
erfasst und übergibt LoadResult.Error
-Objekte an die UI, damit Sie darauf reagieren können. Weitere Informationen zum Bereitstellen des Ladestatus in der UI finden Sie unter Ladestatus verwalten und präsentieren.
Stream von PagingData einrichten
Als Nächstes benötigen Sie einen Stream mit Seitendaten aus der PagingSource
-Implementierung.
Richten Sie den Datenstream in der ViewModel
ein. Die Klasse Pager
bietet Methoden, die einen reaktiven Stream von PagingData
-Objekten aus einer PagingSource
freigeben. Die Paginierungsbibliothek unterstützt die Verwendung mehrerer Streamtypen, darunter Flow
, LiveData
sowie die Typen Flowable
und Observable
von RxJava.
Wenn Sie eine Pager
-Instanz zum Einrichten eines reaktiven Streams erstellen, müssen Sie der Instanz ein PagingConfig
-Konfigurationsobjekt und eine Funktion zur Verfügung stellen, die Pager
mitteilt, wie eine Instanz Ihrer PagingSource
-Implementierung abgerufen wird:
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()
kann der Datenstream geteilt werden und die geladenen Daten werden mit dem bereitgestellten CoroutineScope
im Cache gespeichert. In diesem Beispiel wird die vom Lebenszyklus lifecycle-viewmodel-ktx
-Artefakt bereitgestellte viewModelScope
verwendet.
Das Pager
-Objekt ruft die Methode load()
aus dem PagingSource
-Objekt auf, stellt es mit dem LoadParams
-Objekt bereit und empfängt als Antwort das LoadResult
-Objekt.
RecyclerView-Adapter definieren
Außerdem müssen Sie einen Adapter einrichten, um die Daten in die RecyclerView
-Liste aufzunehmen. Die Paginierungsbibliothek stellt zu diesem Zweck die Klasse PagingDataAdapter
bereit.
Definieren Sie eine Klasse, die PagingDataAdapter
erweitert. Im Beispiel erweitert UserAdapter
PagingDataAdapter
, um einen RecyclerView
-Adapter für Listenelemente vom Typ User
bereitzustellen und UserViewHolder
als Ansichtsinhaber 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 auch die Methoden onCreateViewHolder()
und onBindViewHolder()
definieren und einen DiffUtil.ItemCallback
angeben.
Das funktioniert genauso wie beim Definieren 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); } }
Seitendaten auf Ihrer Benutzeroberfläche anzeigen
Nachdem Sie ein PagingSource
definiert, eine Methode zum Generieren eines PagingData
-Streams durch Ihre App und ein PagingDataAdapter
definiert haben, können Sie diese Elemente verbinden und in Ihrer Aktivität seitenbasierte Daten anzeigen.
Führe die folgenden Schritte in der Methode onCreate
oder der Methode onViewCreated
des Fragments aus:
- Erstellen Sie eine Instanz Ihrer
PagingDataAdapter
-Klasse. - Übergeben Sie die Instanz
PagingDataAdapter
an die ListeRecyclerView
, in der die seitenbasierten Daten angezeigt werden sollen. - Beobachten Sie den
PagingData
-Stream und übergeben Sie jeden generierten Wert an diesubmitData()
-Methode des 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 Daten aus der Datenquelle angezeigt. Bei Bedarf wird automatisch eine weitere Seite geladen.
Weitere Informationen
Weitere Informationen zur Paging-Bibliothek finden Sie in den folgenden Ressourcen:
Codelabs
Produktproben
- Beispiel für das Paging mit den Android-Architekturkomponenten
- Beispiel für das Paging mit Android-Architekturkomponenten
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Seite aus Netzwerk und Datenbank
- Migration zu Paging 3
- Paging-Bibliothek – Übersicht