Nutzerfreundlichkeit verbessern, indem du dafür sorgst, dass deine App verwendet werden kann wenn die Netzwerkverbindungen unzuverlässig sind oder der Nutzer offline ist. Eine Möglichkeit, um gleichzeitig vom Netzwerk und von einer lokalen Datenbank aus zu suchen. Auf diese Weise bedient Ihre Anwendung die UI aus einem lokalen Datenbank-Cache und an das Netzwerk senden, wenn keine Daten mehr in der Datenbank vorhanden sind.
In diesem Leitfaden wird davon ausgegangen, dass Sie mit der Funktion Raumpersistenz vertraut sind. und mit der grundlegenden Verwendung der Paging-Funktion Mediathek.
Koordinatenladevorgänge
Die Paging-Bibliothek bietet die
RemoteMediator
-Komponente
für diesen Anwendungsfall. RemoteMediator
fungiert als Signal von der Paging-Bibliothek
Wenn die App keine im Cache gespeicherten Daten mehr hat. Mit diesem Signal können Sie
zusätzliche Daten aus dem Netzwerk
abrufen und in der lokalen Datenbank speichern,
PagingSource
kann sie laden und
der Benutzeroberfläche zum Anzeigen
bereitgestellt werden.
Wenn zusätzliche Daten benötigt werden, ruft die Paging Library die
Methode load()
von
RemoteMediator
-Implementierung verwenden. Dies ist eine aussetzende Funktion, daher ist sie sicher
lang andauernde Arbeit durchzuführen. Diese Funktion ruft in der Regel die neuen Daten aus
einer Netzwerkquelle
und speichert sie im lokalen Speicher.
Dieser Prozess funktioniert mit neuen Daten, aber im Laufe der Zeit werden die in der Datenbank gespeicherten Daten
erfordert eine Entwertung, z. B. wenn der Nutzer manuell eine Aktualisierung auslöst. Dieses
wird durch LoadType
repräsentiert
-Eigenschaft an die load()
-Methode übergeben. Das LoadType
informiert die
RemoteMediator
, ob die vorhandenen Daten aktualisiert oder
zusätzliche Daten, die der vorhandenen Liste
hinzugefügt oder vorangestellt werden müssen.
Auf diese Weise sorgt das RemoteMediator
dafür, dass Ihre App die Daten lädt, die
die Nutzende in der richtigen
Reihenfolge sehen möchten.
Paging-Lebenszyklus
<ph type="x-smartling-placeholder">![](https://developer.android.google.cn/static/topic/libraries/architecture/images/paging3-base-lifecycle.png?authuser=8&hl=de)
Beim direkten Paging aus dem Netzwerk lädt PagingSource
die Daten und
gibt Folgendes zurück:
LoadResult
-Objekt enthält. Die Implementierung PagingSource
wird an den
Pager
über die
pagingSourceFactory
.
Da für die Benutzeroberfläche neue Daten benötigt werden, ruft Pager
die Methode
Methode load()
aus dem
PagingSource
und gibt einen Stream von
PagingData
-Objekte, die
die neuen Daten zu kapseln. Jedes PagingData
-Objekt wird normalerweise im
ViewModel
, bevor sie zur Anzeige an die UI gesendet werden.
![](https://developer.android.google.cn/static/topic/libraries/architecture/images/paging3-layered-lifecycle.png?authuser=8&hl=de)
RemoteMediator
ändert diesen Datenfluss. Ein PagingSource
lädt die Daten trotzdem.
Sind die Seitendaten jedoch erschöpft, löst die Paging-Bibliothek
RemoteMediator
, um neue Daten aus der Netzwerkquelle zu laden. Das RemoteMediator
speichert die neuen Daten in der lokalen Datenbank, also ein speicherinterner Cache in der
ViewModel
ist nicht erforderlich. Schließlich ungültigt sich PagingSource
selbst und
Der Pager
erstellt eine neue Instanz, um die neuen Daten aus der Datenbank zu laden.
Grundlegende Nutzung
Angenommen, Ihre App soll Seiten von User
Elementen aus einem mit Artikeln zusammenhängenden
Netzwerkdatenquelle in einen lokalen Cache, der in einer Raumdatenbank gespeichert ist.
Mit einer RemoteMediator
-Implementierung können Seitendaten aus dem Netzwerk in
in der Datenbank gespeichert, Daten werden jedoch nicht direkt in die Benutzeroberfläche geladen. Stattdessen verwendet die App
die Datenbank als Quelle
Wahrheit. Mit anderen Worten: Die App
zeigt Daten an, die in der Datenbank zwischengespeichert wurden. Ein PagingSource
Eine Implementierung (z. B. eine von Room generierte) verarbeitet das Laden von Daten aus dem Cache.
aus der Datenbank
in die Benutzeroberfläche.
Elemente des Chatrooms erstellen
Der erste Schritt besteht darin, mit der Funktion Raumpersistenz
Bibliothek zum Definieren einer Datenbank,
lokalen Cache für Paged-Daten aus der Netzwerkdatenquelle. Beginnen Sie mit einer
Implementierung von RoomDatabase
wie unter Speichern von Daten in einer lokalen Datenbank mithilfe
Chatroom.
Definieren Sie als Nächstes eine Raumentität zur Darstellung einer Tabelle mit Listenelementen, wie in
Daten mithilfe von „Room“-Entitäten definieren
Weisen Sie ihr das Feld id
als Primärschlüssel sowie Felder für andere
Informationen, die Ihre Listenelemente enthalten.
Kotlin
@Entity(tableName = "users") data class User(val id: String, val label: String)
Java
@Entity(tableName = "users") public class User { public String id; public String label; }
Java
@Entity(tableName = "users") public class User { public String id; public String label; }
Sie müssen auch ein Datenzugriffsobjekt (Data Access Object, DAO) für diese Raumentität als wie unter Zugriff auf Daten über die Funktion DAOs Der DAO für das Listenelement Entität muss die folgenden Methoden enthalten:
- Eine
insertAll()
-Methode, die eine Liste von Elementen in die Tabelle einfügt. - Eine Methode, die den Abfragestring als Parameter verwendet und einen
PagingSource
-Objekt für die Ergebnisliste. Auf diese Weise kann einPager
-Objekt können Sie diese Tabelle als Quelle für die Seitendaten verwenden. - Eine
clearAll()
-Methode, mit der alle Daten der Tabelle gelöscht werden.
Kotlin
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(users: List<User>) @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> @Query("DELETE FROM users") suspend fun clearAll() }
Java
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List<User> users); @Query("SELECT * FROM users WHERE mLabel LIKE :query") PagingSource<Integer, User> pagingSource(String query); @Query("DELETE FROM users") int clearAll(); }
Java
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List<User> users); @Query("SELECT * FROM users WHERE mLabel LIKE :query") PagingSource<Integer, User> pagingSource(String query); @Query("DELETE FROM users") int clearAll(); }
RemoteMediator implementieren
Die Hauptfunktion von RemoteMediator
besteht darin, mehr Daten aus dem Netzwerk zu laden,
Entweder sind für Pager
die Daten aufgebraucht oder die vorhandenen Daten wurden ungültig gemacht. Es
enthält eine load()
-Methode, die Sie überschreiben müssen, um das Laden
verhalten.
Eine typische RemoteMediator
-Implementierung enthält die folgenden Parameter:
query
: Ein Abfragestring, mit dem definiert wird, welche Daten aus dem Back-End abgerufen werden sollen. .database
: Die Raumdatenbank, die als lokaler Cache dient.networkService
: Eine API-Instanz für den Back-End-Dienst.
Erstellen Sie eine RemoteMediator<Key, Value>
-Implementierung. Der Typ Key
und der
Value
-Typ muss mit dem gleichen Code übereinstimmen wie bei der Definition einer
PagingSource
für dieselbe Netzwerkdatenquelle. Weitere Informationen zu
Typparameter auswählen, siehe Schlüssel und Wert auswählen
.
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { // ... } }
Java
@UseExperimental(markerClass = ExperimentalPagingApi.class) class ExampleRemoteMediator extends RxRemoteMediator<Integer, User> { private String query; private ExampleBackendService networkService; private RoomDb database; private UserDao userDao; ExampleRemoteMediator( String query, ExampleBackendService networkService, RoomDb database ) { query = query; networkService = networkService; database = database; userDao = database.userDao(); } @NotNull @Override public Single<MediatorResult> loadSingle( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { ... } }
Java
class ExampleRemoteMediator extends ListenableFutureRemoteMediator<Integer, User> { private String query; private ExampleBackendService networkService; private RoomDb database; private UserDao userDao; private Executor bgExecutor; ExampleRemoteMediator( String query, ExampleBackendService networkService, RoomDb database, Executor bgExecutor ) { this.query = query; this.networkService = networkService; this.database = database; this.userDao = database.userDao(); this.bgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<MediatorResult> loadFuture( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { ... } }
Die Methode load()
ist für die Aktualisierung des unterstützenden Datasets und
PagingSource
wird ungültig. Einige Bibliotheken, die Paging unterstützen (z. B. Room)
verarbeitet automatisch die Entwertung von PagingSource
-Objekten,
umsetzen.
Die Methode load()
akzeptiert zwei Parameter:
PagingState
mit Informationen über die bisher geladenen Seiten, den zuletzt aufgerufenen Index und diePagingConfig
-Objekt, das Sie zum Initialisieren des Paging-Streams verwendet haben.LoadType
, was den Typ der Last:REFRESH
,APPEND
oderPREPEND
Der Rückgabewert der Methode load()
ist ein
MediatorResult
-Objekt enthält. MediatorResult
kann entweder
MediatorResult.Error
(einschließlich Fehlerbeschreibung) oder
MediatorResult.Success
(dazu gehört ein Signal, das angibt, ob mehr Daten geladen werden müssen)
Die Methode load()
muss die folgenden Schritte ausführen:
- Bestimmen Sie je nach Ladetyp und die bisher geladen wurden.
- Netzwerkanfrage auslösen
- Führen Sie je nach Ergebnis des Ladevorgangs Aktionen aus:
<ph type="x-smartling-placeholder">
- </ph>
- Wenn der Ladevorgang erfolgreich ist und die Liste der empfangenen Artikel nicht leer ist,
speichern Sie die Listenelemente in der Datenbank
MediatorResult.Success(endOfPaginationReached = false)
Nach den Daten gespeichert ist, entwerten Sie die Datenquelle, um die Paging-Bibliothek neuen Daten. - Wenn der Ladevorgang erfolgreich ist und entweder die Liste der empfangenen Elemente leer ist
oder den Index der letzten Seite.
MediatorResult.Success(endOfPaginationReached = true)
Nachdem die Daten gespeichert haben, entwerten Sie die Datenquelle, um die Paging-Bibliothek über das neue Daten. - Wenn die Anfrage einen Fehler verursacht, wird
MediatorResult.Error
zurückgegeben.
- Wenn der Ladevorgang erfolgreich ist und die Liste der empfangenen Artikel nicht leer ist,
speichern Sie die Listenelemente in der Datenbank
Kotlin
override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // The network load method takes an optional after=<user.id> // parameter. For every page after the first, pass the last user // ID to let it continue from where it left off. For REFRESH, // pass null to load the first page. val loadKey = when (loadType) { LoadType.REFRESH -> null // In this example, you never need to prepend, since REFRESH // will always load the first page in the list. Immediately // return, reporting end of pagination. LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true) LoadType.APPEND -> { val lastItem = state.lastItemOrNull() // You must explicitly check if the last item is null when // appending, since passing null to networkService is only // valid for initial load. If lastItem is null it means no // items were loaded after the initial REFRESH and there are // no more items to load. if (lastItem == null) { return MediatorResult.Success( endOfPaginationReached = true ) } lastItem.id } } // Suspending network load via Retrofit. This doesn't need to be // wrapped in a withContext(Dispatcher.IO) { ... } block since // Retrofit's Coroutine CallAdapter dispatches on a worker // thread. val response = networkService.searchUsers( query = query, after = loadKey ) database.withTransaction { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query) } // Insert new users into database, which invalidates the // current PagingData, allowing Paging to present the updates // in the DB. userDao.insertAll(response.users) } MediatorResult.Success( endOfPaginationReached = response.nextKey == null ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } }
Java
@NotNull @Override public Single<MediatorResult> loadSingle( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional after=<user.id> parameter. For // every page after the first, pass the last user ID to let it continue from // where it left off. For REFRESH, pass null to load the first page. String loadKey = null; switch (loadType) { case REFRESH: break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Single.just(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when appending, // since passing null to networkService is only valid for initial load. // If lastItem is null it means no items were loaded after the initial // REFRESH and there are no more items to load. if (lastItem == null) { return Single.just(new MediatorResult.Success(true)); } loadKey = lastItem.getId(); break; } return networkService.searchUsers(query, loadKey) .subscribeOn(Schedulers.io()) .map((Function<SearchUserResponse, MediatorResult>) response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); } // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getNextKey() == null); }) .onErrorResumeNext(e -> { if (e instanceof IOException || e instanceof HttpException) { return Single.just(new MediatorResult.Error(e)); } return Single.error(e); }); }
Java
@NotNull @Override public ListenableFuture<MediatorResult> loadFuture( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional after=<user.id> parameter. For // every page after the first, pass the last user ID to let it continue from // where it left off. For REFRESH, pass null to load the first page. String loadKey = null; switch (loadType) { case REFRESH: break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Futures.immediateFuture(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when appending, // since passing null to networkService is only valid for initial load. // If lastItem is null it means no items were loaded after the initial // REFRESH and there are no more items to load. if (lastItem == null) { return Futures.immediateFuture(new MediatorResult.Success(true)); } loadKey = lastItem.getId(); break; } ListenableFuture<MediatorResult> networkResult = Futures.transform( networkService.searchUsers(query, loadKey), response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); } // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getNextKey() == null); }, bgExecutor); ListenableFuture<MediatorResult> ioCatchingNetworkResult = Futures.catching( networkResult, IOException.class, MediatorResult.Error::new, bgExecutor ); return Futures.catching( ioCatchingNetworkResult, HttpException.class, MediatorResult.Error::new, bgExecutor ); }
Initialisierungsmethode definieren
RemoteMediator
-Implementierungen können auch den
initialize()
um zu prüfen, ob im Cache gespeicherte Daten veraltet sind, und zu entscheiden, ob sie ausgelöst werden soll oder nicht.
eine Remote-Aktualisierung durchführen. Diese Methode wird vor dem Laden ausgeführt,
Manipulieren der Datenbank (z. B. um alte Daten zu löschen), bevor
lokale oder Remote-Ladevorgänge.
Da initialize()
eine asynchrone Funktion ist, können Sie Daten in
die Relevanz der in der Datenbank vorhandenen Daten zu bestimmen. Am häufigsten
Daten im Cache sind nur für einen bestimmten Zeitraum gültig. Die
RemoteMediator
kann prüfen, ob diese Ablaufzeit verstrichen ist.
falls die Paging-Bibliothek die Daten vollständig aktualisieren muss. Implementierungen von
initialize()
sollte so eine InitializeAction
zurückgeben:
- Wenn die lokalen Daten vollständig aktualisiert werden müssen,
initialize()
sollte Folgendes zurückgeben:InitializeAction.LAUNCH_INITIAL_REFRESH
Dies führt dazu, dassRemoteMediator
eine Remote-Aktualisierung durchführt, um vollständig neu geladen zu werden. mit den Daten. Alle Remote-APPEND
- oderPREPEND
-Ladevorgänge warten auf das Laden vonREFRESH
. um erfolgreich zu sein, bevor Sie fortfahren. - Falls die lokalen Daten nicht aktualisiert werden müssen,
initialize()
sollte Folgendes zurückgeben:InitializeAction.SKIP_INITIAL_REFRESH
Dadurch überspringtRemoteMediator
die Remote-Aktualisierung und lädt die Daten im Cache gespeichert.
Kotlin
override suspend fun initialize(): InitializeAction { val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS) return if (System.currentTimeMillis() - db.lastUpdated() <= cacheTimeout) { // Cached data is up-to-date, so there is no need to re-fetch // from the network. InitializeAction.SKIP_INITIAL_REFRESH } else { // Need to refresh cached data from network; returning // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's // APPEND and PREPEND from running until REFRESH succeeds. InitializeAction.LAUNCH_INITIAL_REFRESH } }
Java
@NotNull @Override public Single<InitializeAction> initializeSingle() { long cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); return mUserDao.lastUpdatedSingle() .map(lastUpdatedMillis -> { if (System.currentTimeMillis() - lastUpdatedMillis <= cacheTimeout) { // Cached data is up-to-date, so there is no need to re-fetch // from the network. return InitializeAction.SKIP_INITIAL_REFRESH; } else { // Need to refresh cached data from network; returning // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's // APPEND and PREPEND from running until REFRESH succeeds. return InitializeAction.LAUNCH_INITIAL_REFRESH; } }); }
Java
@NotNull @Override public ListenableFuture<InitializeAction> initializeFuture() { long cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS); return Futures.transform( mUserDao.lastUpdated(), lastUpdatedMillis -> { if (System.currentTimeMillis() - lastUpdatedMillis <= cacheTimeout) { // Cached data is up-to-date, so there is no need to re-fetch // from the network. return InitializeAction.SKIP_INITIAL_REFRESH; } else { // Need to refresh cached data from network; returning // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's // APPEND and PREPEND from running until REFRESH succeeds. return InitializeAction.LAUNCH_INITIAL_REFRESH; } }, mBgExecutor); }
Pager erstellen
Abschließend müssen Sie eine Pager
-Instanz erstellen, um den Stream von Seitendaten einzurichten.
Dies ähnelt dem Erstellen einer Pager
aus einer einfachen Netzwerkdatenquelle, aber
müssen Sie zwei Dinge anders machen:
- Anstatt einen
PagingSource
-Konstruktor direkt zu übergeben, müssen Sie den , die einPagingSource
-Objekt aus dem DAO zurückgibt. - Sie müssen eine Instanz Ihrer
RemoteMediator
-Implementierung als dasremoteMediator
-Parameter.
Kotlin
val userDao = database.userDao() val pager = Pager( config = PagingConfig(pageSize = 50) remoteMediator = ExampleRemoteMediator(query, database, networkService) ) { userDao.pagingSource(query) }
Java
UserDao userDao = database.userDao(); Pager<Integer, User> pager = Pager( new PagingConfig(/* pageSize = */ 20), null, // initialKey, new ExampleRemoteMediator(query, database, networkService) () -> userDao.pagingSource(query));
Java
UserDao userDao = database.userDao(); Pager<Integer, User> pager = Pager( new PagingConfig(/* pageSize = */ 20), null, // initialKey new ExampleRemoteMediator(query, database, networkService, bgExecutor), () -> userDao.pagingSource(query));
Race-Bedingungen verarbeiten
Eine Situation, die Ihre App bewältigen muss, wenn Sie Daten aus mehreren sind lokal im Cache gespeicherte Daten nicht mehr synchron mit dem Remote-Datenquelle.
Wenn die Methode initialize()
aus Ihrer RemoteMediator
-Implementierung Folgendes zurückgibt:
LAUNCH_INITIAL_REFRESH
, die Daten sind veraltet und müssen durch neue ersetzt werden
Daten. Alle PREPEND
- oder APPEND
-Ladeanfragen werden gezwungen, auf die Remote-
REFRESH
geladen, um erfolgreich zu sein. Weil die PREPEND
- oder APPEND
-Anfragen
vor der REFRESH
-Anfrage in die Warteschlange gestellt wurde, ist es möglich, dass PagingState
an diese Ladeaufrufe übergeben wurden, sind zum Zeitpunkt ihrer Ausführung veraltet.
Je nachdem, wie die Daten lokal gespeichert werden, kann Ihre App redundante
-Anfragen, wenn Änderungen an den im Cache gespeicherten Daten zur Entwertung und neuen Datenabrufen führen.
So werden z. B. Abfragen für eingefügte Daten ungültig. Das bedeutet, dass neue
PagingSource
-Objekte mit den aktualisierten Daten werden für den ausstehenden Ladevorgang bereitgestellt
-Anfragen, wenn neue Daten in die Datenbank eingefügt werden.
Die Lösung dieses Datensynchronisierungsproblems ist unerlässlich, um sicherzustellen, die relevantesten und aktuellsten Daten zu sehen. Die beste Lösung hängt hauptsächlich wie die Datenquelle des Netzwerks die Daten ausgibt. In jedem Fall sollte remote Schlüssel ermöglichen das Speichern von Informationen zur letzten Seite. die vom Server angefordert werden. Anhand dieser Informationen kann die App fordern Sie die richtige Datenseite an, die als Nächstes geladen werden soll.
Fernbedienungstasten verwalten
Remote-Schlüssel sind Schlüssel, mit denen eine RemoteMediator
-Implementierung dem
Back-End-Dienst, welche Daten als Nächstes geladen werden sollen. Im einfachsten Fall kann jedes Element
enthält eine Remote-Taste, auf die Sie ganz einfach verweisen können. Wenn die
Remote-Tasten nicht einzelnen Elementen entsprechen, müssen Sie diese speichern.
separat verwalten und in der load()
-Methode verwalten.
In diesem Abschnitt wird beschrieben, wie Sie Remoteschlüssel erfassen, speichern und aktualisieren können, nicht in einzelnen Elementen gespeichert.
Artikelschlüssel
In diesem Abschnitt wird beschrieben, wie Sie mit Remote-Tasten arbeiten, die den entsprechenden
einzelne Elemente. Wenn ein API einzelne Elemente ausschlüsselt, wird das Element normalerweise
Die ID wird als Suchparameter übergeben. Der Parametername gibt an, ob der
mit Artikeln vor oder nach der angegebenen ID antworten sollte. Im Beispiel
der Modellklasse User
wird das Feld id
vom Server als Remote-
wenn Sie zusätzliche Daten anfordern.
Wenn mit der Methode load()
artikelspezifische Remote-Tasten verwaltet werden müssen, sind diese Tasten
sind normalerweise die IDs der vom Server abgerufenen Daten. Aktualisierungsvorgänge
benötigen keinen Ladeschlüssel, da sie nur die neuesten Daten abrufen.
Ebenso müssen
Vorgänge beim Voranstellen
keine zusätzlichen Daten abgerufen werden,
Bei Aktualisierung werden immer die neuesten Daten vom Server abgerufen.
Für Anfügevorgänge ist jedoch eine ID erforderlich. Dazu müssen Sie die letzte
aus der Datenbank und laden Sie mit seiner ID die nächste Seite mit Daten. Wenn es
keine Elemente in der Datenbank vorhanden sind, wird endOfPaginationReached
auf "true" gesetzt,
gibt an, dass die Daten aktualisiert werden müssen.
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // The network load method takes an optional String // parameter. For every page after the first, pass the String // token returned from the previous page to let it continue // from where it left off. For REFRESH, pass null to load the // first page. val loadKey = when (loadType) { LoadType.REFRESH -> null // In this example, you never need to prepend, since REFRESH // will always load the first page in the list. Immediately // return, reporting end of pagination. LoadType.PREPEND -> return MediatorResult.Success( endOfPaginationReached = true ) // Get the last User object id for the next RemoteKey. LoadType.APPEND -> { val lastItem = state.lastItemOrNull() // You must explicitly check if the last item is null when // appending, since passing null to networkService is only // valid for initial load. If lastItem is null it means no // items were loaded after the initial REFRESH and there are // no more items to load. if (lastItem == null) { return MediatorResult.Success( endOfPaginationReached = true ) } lastItem.id } } // Suspending network load via Retrofit. This doesn't need to // be wrapped in a withContext(Dispatcher.IO) { ... } block // since Retrofit's Coroutine CallAdapter dispatches on a // worker thread. val response = networkService.searchUsers(query, loadKey) // Store loaded data, and next key in transaction, so that // they're always consistent. database.withTransaction { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query) } // Insert new users into database, which invalidates the // current PagingData, allowing Paging to present the updates // in the DB. userDao.insertAll(response.users) } // End of pagination has been reached if no users are returned from the // service MediatorResult.Success( endOfPaginationReached = response.users.isEmpty() ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } } }
Java
@NotNull @Override public Single>MediatorResult< loadSingle( @NotNull LoadType loadType, @NotNull PagingState>Integer, User< state ) { // The network load method takes an optional String parameter. For every page // after the first, pass the String token returned from the previous page to // let it continue from where it left off. For REFRESH, pass null to load the // first page. Single>String< remoteKeySingle = null; switch (loadType) { case REFRESH: // Initial load should use null as the page key, so you can return null // directly. remoteKeySingle = Single.just(null); break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Single.just(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when // appending, since passing null to networkService is only // valid for initial load. If lastItem is null it means no // items were loaded after the initial REFRESH and there are // no more items to load. if (lastItem == null) { return Single.just(new MediatorResult.Success(true)); } remoteKeySingle = Single.just(lastItem.getId()); break; } return remoteKeySingle .subscribeOn(Schedulers.io()) .flatMap((Function<String, Single<MediatorResult>>) remoteKey -> { return networkService.searchUsers(query, remoteKey) .map(response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); } // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getUsers().isEmpty()); }); }) .onErrorResumeNext(e -> { if (e instanceof IOException || e instanceof HttpException) { return Single.just(new MediatorResult.Error(e)); } return Single.error(e); }); }
Java
@NotNull @Override public ListenableFuture<MediatorResult> loadFuture( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional after=<user.id> parameter. // For every page after the first, pass the last user ID to let it continue // from where it left off. For REFRESH, pass null to load the first page. ResolvableFuture<String> remoteKeyFuture = ResolvableFuture.create(); switch (loadType) { case REFRESH: remoteKeyFuture.set(null); break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Futures.immediateFuture(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when appending, // since passing null to networkService is only valid for initial load. // If lastItem is null it means no items were loaded after the initial // REFRESH and there are no more items to load. if (lastItem == null) { return Futures.immediateFuture(new MediatorResult.Success(true)); } remoteKeyFuture.set(lastItem.getId()); break; } return Futures.transformAsync(remoteKeyFuture, remoteKey -> { ListenableFuture<MediatorResult> networkResult = Futures.transform( networkService.searchUsers(query, remoteKey), response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); } // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getUsers().isEmpty()); }, bgExecutor); ListenableFuture<MediatorResult> ioCatchingNetworkResult = Futures.catching( networkResult, IOException.class, MediatorResult.Error::new, bgExecutor ); return Futures.catching( ioCatchingNetworkResult, HttpException.class, MediatorResult.Error::new, bgExecutor ); }, bgExecutor); }
Seitentasten
In diesem Abschnitt wird beschrieben, wie Sie mit Remote-Tasten arbeiten, die nicht der einzelne Elemente.
Tabelle mit Remote-Schlüsseln hinzufügen
Wenn die Fernbedienungstasten nicht direkt mit Listenelementen verknüpft sind, in einer separaten Tabelle in der lokalen Datenbank speichern. Definieren Sie eine Raumentität, die stellt eine Tabelle mit Remote-Tasten dar:
Kotlin
@Entity(tableName = "remote_keys") data class RemoteKey(val label: String, val nextKey: String?)
Java
@Entity(tableName = "remote_keys") public class RemoteKey { public String label; public String nextKey; }
Java
@Entity(tableName = "remote_keys") public class RemoteKey { public String label; public String nextKey; }
Sie müssen auch einen DAO für die Entität RemoteKey
definieren:
Kotlin
@Dao interface RemoteKeyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertOrReplace(remoteKey: RemoteKey) @Query("SELECT * FROM remote_keys WHERE label = :query") suspend fun remoteKeyByQuery(query: String): RemoteKey @Query("DELETE FROM remote_keys WHERE label = :query") suspend fun deleteByQuery(query: String) }
Java
@Dao interface RemoteKeyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertOrReplace(RemoteKey remoteKey); @Query("SELECT * FROM remote_keys WHERE label = :query") Single<RemoteKey> remoteKeyByQuerySingle(String query); @Query("DELETE FROM remote_keys WHERE label = :query") void deleteByQuery(String query); }
Java
@Dao interface RemoteKeyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertOrReplace(RemoteKey remoteKey); @Query("SELECT * FROM remote_keys WHERE label = :query") ListenableFuture<RemoteKey> remoteKeyByQueryFuture(String query); @Query("DELETE FROM remote_keys WHERE label = :query") void deleteByQuery(String query); }
Mit Remote-Tasten laden
Wenn mit der Methode load()
Remote-Seitenschlüssel verwaltet werden müssen, müssen Sie sie definieren
anders als bei der grundlegenden Nutzung
RemoteMediator
:
- Fügen Sie eine zusätzliche Property hinzu, die einen Verweis auf den DAO für Ihr Tabelle mit Remote-Schlüsseln.
- Bestimmen Sie, welcher Schlüssel als Nächstes geladen werden soll, indem Sie die Remote-Schlüsseltabelle abfragen
mit
PagingState
. - Fügen Sie den zurückgegebenen Remoteschlüssel aus der Netzwerkdatenquelle ein oder speichern Sie ihn in zusätzlich zu den Seitendaten selbst.
Kotlin
@OptIn(ExperimentalPagingApi::class) class ExampleRemoteMediator( private val query: String, private val database: RoomDb, private val networkService: ExampleBackendService ) : RemoteMediator<Int, User>() { val userDao = database.userDao() val remoteKeyDao = database.remoteKeyDao() override suspend fun load( loadType: LoadType, state: PagingState<Int, User> ): MediatorResult { return try { // The network load method takes an optional String // parameter. For every page after the first, pass the String // token returned from the previous page to let it continue // from where it left off. For REFRESH, pass null to load the // first page. val loadKey = when (loadType) { LoadType.REFRESH -> null // In this example, you never need to prepend, since REFRESH // will always load the first page in the list. Immediately // return, reporting end of pagination. LoadType.PREPEND -> return MediatorResult.Success( endOfPaginationReached = true ) // Query remoteKeyDao for the next RemoteKey. LoadType.APPEND -> { val remoteKey = database.withTransaction { remoteKeyDao.remoteKeyByQuery(query) } // You must explicitly check if the page key is null when // appending, since null is only valid for initial load. // If you receive null for APPEND, that means you have // reached the end of pagination and there are no more // items to load. if (remoteKey.nextKey == null) { return MediatorResult.Success( endOfPaginationReached = true ) } remoteKey.nextKey } } // Suspending network load via Retrofit. This doesn't need to // be wrapped in a withContext(Dispatcher.IO) { ... } block // since Retrofit's Coroutine CallAdapter dispatches on a // worker thread. val response = networkService.searchUsers(query, loadKey) // Store loaded data, and next key in transaction, so that // they're always consistent. database.withTransaction { if (loadType == LoadType.REFRESH) { remoteKeyDao.deleteByQuery(query) userDao.deleteByQuery(query) } // Update RemoteKey for this query. remoteKeyDao.insertOrReplace( RemoteKey(query, response.nextKey) ) // Insert new users into database, which invalidates the // current PagingData, allowing Paging to present the updates // in the DB. userDao.insertAll(response.users) } MediatorResult.Success( endOfPaginationReached = response.nextKey == null ) } catch (e: IOException) { MediatorResult.Error(e) } catch (e: HttpException) { MediatorResult.Error(e) } } }
Java
@NotNull @Override public Single<MediatorResult> loadSingle( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional String parameter. For every page // after the first, pass the String token returned from the previous page to // let it continue from where it left off. For REFRESH, pass null to load the // first page. Single<RemoteKey> remoteKeySingle = null; switch (loadType) { case REFRESH: // Initial load should use null as the page key, so you can return null // directly. remoteKeySingle = Single.just(new RemoteKey(mQuery, null)); break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Single.just(new MediatorResult.Success(true)); case APPEND: // Query remoteKeyDao for the next RemoteKey. remoteKeySingle = mRemoteKeyDao.remoteKeyByQuerySingle(mQuery); break; } return remoteKeySingle .subscribeOn(Schedulers.io()) .flatMap((Function<RemoteKey, Single<MediatorResult>>) remoteKey -> { // You must explicitly check if the page key is null when appending, // since null is only valid for initial load. If you receive null // for APPEND, that means you have reached the end of pagination and // there are no more items to load. if (loadType != REFRESH && remoteKey.getNextKey() == null) { return Single.just(new MediatorResult.Success(true)); } return networkService.searchUsers(query, remoteKey.getNextKey()) .map(response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); remoteKeyDao.deleteByQuery(query); } // Update RemoteKey for this query. remoteKeyDao.insertOrReplace(new RemoteKey(query, response.getNextKey())); // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getNextKey() == null); }); }) .onErrorResumeNext(e -> { if (e instanceof IOException || e instanceof HttpException) { return Single.just(new MediatorResult.Error(e)); } return Single.error(e); }); }
Java
@NotNull @Override public ListenableFuture<MediatorResult> loadFuture( @NotNull LoadType loadType, @NotNull PagingState<Integer, User> state ) { // The network load method takes an optional after=<user.id> parameter. For // every page after the first, pass the last user ID to let it continue from // where it left off. For REFRESH, pass null to load the first page. ResolvableFuture<RemoteKey> remoteKeyFuture = ResolvableFuture.create(); switch (loadType) { case REFRESH: remoteKeyFuture.set(new RemoteKey(query, null)); break; case PREPEND: // In this example, you never need to prepend, since REFRESH will always // load the first page in the list. Immediately return, reporting end of // pagination. return Futures.immediateFuture(new MediatorResult.Success(true)); case APPEND: User lastItem = state.lastItemOrNull(); // You must explicitly check if the last item is null when appending, // since passing null to networkService is only valid for initial load. // If lastItem is null it means no items were loaded after the initial // REFRESH and there are no more items to load. if (lastItem == null) { return Futures.immediateFuture(new MediatorResult.Success(true)); } // Query remoteKeyDao for the next RemoteKey. remoteKeyFuture.setFuture( remoteKeyDao.remoteKeyByQueryFuture(query)); break; } return Futures.transformAsync(remoteKeyFuture, remoteKey -> { // You must explicitly check if the page key is null when appending, // since null is only valid for initial load. If you receive null // for APPEND, that means you have reached the end of pagination and // there are no more items to load. if (loadType != LoadType.REFRESH && remoteKey.getNextKey() == null) { return Futures.immediateFuture(new MediatorResult.Success(true)); } ListenableFuture<MediatorResult> networkResult = Futures.transform( networkService.searchUsers(query, remoteKey.getNextKey()), response -> { database.runInTransaction(() -> { if (loadType == LoadType.REFRESH) { userDao.deleteByQuery(query); remoteKeyDao.deleteByQuery(query); } // Update RemoteKey for this query. remoteKeyDao.insertOrReplace(new RemoteKey(query, response.getNextKey())); // Insert new users into database, which invalidates the current // PagingData, allowing Paging to present the updates in the DB. userDao.insertAll(response.getUsers()); }); return new MediatorResult.Success(response.getNextKey() == null); }, bgExecutor); ListenableFuture<MediatorResult> ioCatchingNetworkResult = Futures.catching( networkResult, IOException.class, MediatorResult.Error::new, bgExecutor ); return Futures.catching( ioCatchingNetworkResult, HttpException.class, MediatorResult.Error::new, bgExecutor ); }, bgExecutor); }
Vor Ort aktualisieren
Wenn Ihre App nur Netzwerkaktualisierungen von oben in der Liste als
vorherigen Beispielen gezeigt haben, muss Ihr RemoteMediator
nicht definieren,
Ladeverhalten voranstellen.
Wenn Ihre App jedoch
das inkrementelle Laden aus dem Netzwerk unterstützen muss,
in die lokale Datenbank importieren, müssen Sie die Paginierung
beginnend am Anker, der Scrollposition der Nutzenden. PagingSource
des Raums
die Implementierung übernimmt. Wenn Sie den Raum nicht nutzen,
indem Sie die
PagingSource.getRefreshKey()
Ein Beispiel für die Implementierung von getRefreshKey()
finden Sie unter Definieren des
PagingSource.
In Abbildung 4 sehen Sie, wie Daten zuerst aus der lokalen Datenbank geladen werden. und dann aus dem Netzwerk, sobald die Daten der Datenbank aufgebraucht sind.
<ph type="x-smartling-placeholder">Weitere Informationen
Weitere Informationen zur Paging-Bibliothek finden Sie in den folgenden zusätzlichen Ressourcen:
Codelabs
Produktproben
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Daten aus Seiten laden und anzeigen
- Paging-Implementierung testen
- Zu Paging 3 migrieren