Verbessern Sie die Nutzerfreundlichkeit, indem Sie dafür sorgen, dass Ihre App auch dann verwendet werden kann, wenn die Netzwerkverbindungen unzuverlässig sind oder der Nutzer offline ist. Eine Möglichkeit besteht darin, gleichzeitig aus dem Netzwerk und aus einer lokalen Datenbank auszusortieren. Auf diese Weise führt Ihre Anwendung die UI aus einem lokalen Datenbankcache aus und stellt nur dann Anfragen an das Netzwerk, wenn sich keine Daten mehr in der Datenbank befinden.
In diesem Leitfaden wird davon ausgegangen, dass Sie mit der Raumpersistenzbibliothek und der grundlegenden Verwendung der Paging-Bibliothek vertraut sind.
Laden von Koordinatendaten
Die Paginierungsbibliothek stellt die Komponente RemoteMediator
für diesen Anwendungsfall bereit. RemoteMediator
fungiert als Signal von der Paging-Bibliothek, wenn die Anwendung keine im Cache gespeicherten Daten mehr hat. Mit diesem Signal können Sie zusätzliche Daten aus dem Netzwerk laden und in der lokalen Datenbank speichern, wo sie von einem PagingSource
geladen und der UI zur Anzeige bereitgestellt werden können.
Wenn zusätzliche Daten benötigt werden, ruft die Paginierungsbibliothek die Methode load()
aus der RemoteMediator
-Implementierung auf. Dies ist eine Anhaltende Funktion, sodass lang andauernde Arbeiten sicher ausgeführt werden können. Diese Funktion ruft die neuen Daten normalerweise von einer Netzwerkquelle ab und speichert sie im lokalen Speicher.
Dieser Prozess funktioniert mit neuen Daten, aber mit der Zeit müssen die in der Datenbank gespeicherten Daten entwertet werden, z. B. wenn der Nutzer manuell eine Aktualisierung auslöst. Dies wird durch das Attribut LoadType
dargestellt, das an die Methode load()
übergeben wird. Das LoadType
informiert die RemoteMediator
darüber, ob sie die vorhandenen Daten aktualisieren oder zusätzliche Daten abrufen muss, die der vorhandenen Liste angehängt oder vorangestellt werden müssen.
Auf diese Weise sorgt RemoteMediator
dafür, dass Ihre Anwendung die Daten lädt, die die Nutzer sehen möchten, in der richtigen Reihenfolge.
Paginglebenszyklus
![](https://developer.android.com/static/topic/libraries/architecture/images/paging3-base-lifecycle.png?authuser=6&hl=de)
Beim Paging direkt über das Netzwerk lädt PagingSource
die Daten und gibt ein LoadResult
-Objekt zurück. Die Implementierung PagingSource
wird über den Parameter pagingSourceFactory
an Pager
übergeben.
Wenn von der UI neue Daten benötigt werden, ruft der Pager
die Methode load()
aus PagingSource
auf und gibt einen Stream von PagingData
-Objekten zurück, in denen die neuen Daten zusammengefasst werden. Jedes PagingData
-Objekt wird normalerweise im ViewModel
zwischengespeichert, bevor es zur Anzeige an die UI gesendet wird.
![](https://developer.android.com/static/topic/libraries/architecture/images/paging3-layered-lifecycle.png?authuser=6&hl=de)
RemoteMediator
ändert diesen Datenfluss. Ein PagingSource
lädt die Daten weiterhin. Wenn die ausgelagerten Daten jedoch erschöpft sind, veranlasst die Paging-Bibliothek das RemoteMediator
, neue Daten aus der Netzwerkquelle zu laden. Der RemoteMediator
speichert die neuen Daten in der lokalen Datenbank, sodass kein speicherinterner Cache im ViewModel
erforderlich ist. Schließlich wird das PagingSource
automatisch ungültig und der Pager
erstellt eine neue Instanz, um die neuen Daten aus der Datenbank zu laden.
Grundlegende Verwendung
Angenommen, Sie möchten, dass Ihre Anwendung Seiten mit User
Elementen aus einer mit Artikeln gebundenen Netzwerkdatenquelle in einen lokalen Cache lädt, der in einer Raumdatenbank gespeichert ist.
Mit einer RemoteMediator
-Implementierung werden Daten aus Seiten aus dem Netzwerk in die Datenbank geladen. Die Daten werden jedoch nicht direkt in die UI geladen. Stattdessen verwendet die Anwendung die Datenbank als Datenquelle. Mit anderen Worten: Die Anwendung zeigt nur Daten an, die in der Datenbank zwischengespeichert wurden. Eine PagingSource
-Implementierung (z. B. eine von Room generierte) übernimmt das Laden von im Cache gespeicherten Daten aus der Datenbank in die UI.
Raumentitäten erstellen
Im ersten Schritt verwenden Sie die Room Persistence Library, um eine Datenbank zu definieren, die einen lokalen Cache mit ausgelagerten Daten aus der Netzwerkdatenquelle enthält. Beginnen Sie mit einer Implementierung von RoomDatabase
, wie unter Daten in einer lokalen Datenbank mit Raum speichern beschrieben.
Definieren Sie als Nächstes eine Zimmerentität, um eine Tabelle mit Listenelementen darzustellen, wie unter Daten mithilfe von Zimmerentitäten definieren beschrieben.
Weisen Sie ihr ein id
-Feld als Primärschlüssel sowie Felder für alle anderen Informationen in Ihren Listenelementen zu.
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; }
Außerdem müssen Sie ein Datenzugriffsobjekt (Data Access Object, DAO) für diese Zimmerentität definieren, wie unter Mit Raum-DAOs auf Daten zugreifen beschrieben. Die DAO für die Listenelemententität muss die folgenden Methoden enthalten:
- Eine
insertAll()
-Methode, mit der eine Liste von Elementen in die Tabelle eingefügt wird. - Eine Methode, die den Abfragestring als Parameter verwendet und ein
PagingSource
-Objekt für die Liste der Ergebnisse zurückgibt. So kann einPager
-Objekt diese Tabelle als Quelle für ausgelagerte Daten verwenden. - Eine
clearAll()
-Methode, die alle Daten der Tabelle löscht.
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(); }
Remote-Mediator implementieren
Die Hauptaufgabe von RemoteMediator
besteht darin, mehr Daten aus dem Netzwerk zu laden, wenn entweder keine Daten mehr im Pager
vorhanden sind oder die vorhandenen Daten ungültig werden. Sie enthält die Methode load()
, die Sie überschreiben müssen, um das Ladeverhalten zu definieren.
Eine typische RemoteMediator
-Implementierung umfasst die folgenden Parameter:
query
: Ein Abfragestring, der definiert, welche Daten aus dem Back-End-Dienst 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. Die Typen Key
und Value
sollten mit denen identisch sein, wenn Sie eine PagingSource
für dieselbe Netzwerkdatenquelle definieren würden. Weitere Informationen zum Auswählen von Typparametern finden Sie unter Schlüssel- und Werttypen 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 Back-End-Datasets und die Entwertung des PagingSource
zuständig. Einige Bibliotheken, die Paging unterstützen (z. B. Room), verarbeiten ungültige PagingSource
-Objekte, die sie implementieren, automatisch.
Die Methode load()
verwendet zwei Parameter:
PagingState
mit Informationen zu den bisher geladenen Seiten, dem zuletzt aufgerufenen Index und demPagingConfig
-Objekt, mit dem Sie den Paging-Stream initialisiert haben.LoadType
, der den Typ der Last angibt:REFRESH
,APPEND
oderPREPEND
.
Der Rückgabewert der Methode load()
ist ein MediatorResult
-Objekt. MediatorResult
kann entweder MediatorResult.Error
(mit Fehlerbeschreibung) oder MediatorResult.Success
(mit einem Signal, das angibt, ob weitere Daten zum Laden vorhanden sind) sein.
Die Methode load()
muss die folgenden Schritte ausführen:
- Bestimmen Sie je nach Ladetyp und den bisher geladenen Daten, welche Seite aus dem Netzwerk geladen werden soll.
- Lösen Sie die Netzwerkanfrage aus.
- Führen Sie die Aktionen je nach Ergebnis des Ladevorgangs aus:
- Wenn der Ladevorgang erfolgreich ist und die empfangene Elementliste nicht leer ist, speichern Sie die Listenelemente in der Datenbank und geben Sie
MediatorResult.Success(endOfPaginationReached = false)
zurück. Nach dem Speichern der Daten müssen Sie die Datenquelle entwerten, um die Paging-Bibliothek über die neuen Daten zu informieren. - Wenn das Laden erfolgreich ist und entweder die empfangene Elementliste leer ist oder der letzte Seitenindex vorliegt, geben Sie
MediatorResult.Success(endOfPaginationReached = true)
zurück. Nach dem Speichern der Daten müssen Sie die Datenquelle entwerten, um die Paging-Bibliothek über die neuen Daten zu informieren. - Wenn die Anfrage einen Fehler verursacht, wird
MediatorResult.Error
zurückgegeben.
- Wenn der Ladevorgang erfolgreich ist und die empfangene Elementliste nicht leer ist, speichern Sie die Listenelemente in der Datenbank und geben Sie
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 die Methode initialize()
überschreiben, um zu prüfen, ob im Cache gespeicherte Daten veraltet sind, und um zu entscheiden, ob eine Remote-Aktualisierung ausgelöst werden soll. Diese Methode wird vor dem Laden ausgeführt. So können Sie die Datenbank bearbeiten (z. B. um alte Daten zu löschen), bevor lokale oder Remote-Ladevorgänge ausgelöst werden.
Da es sich bei initialize()
um eine asynchrone Funktion handelt, können Sie Daten laden, um die Relevanz der vorhandenen Daten in der Datenbank zu ermitteln. Meistens sind die im Cache gespeicherten Daten nur für einen bestimmten Zeitraum gültig. Der RemoteMediator
kann prüfen, ob diese Ablaufzeit abgelaufen ist. In diesem Fall muss die Paginierungsbibliothek die Daten vollständig aktualisieren. Bei Implementierungen von initialize()
sollte wie folgt ein InitializeAction
zurückgegeben werden:
- Falls die lokalen Daten vollständig aktualisiert werden müssen, sollte
initialize()
InitializeAction.LAUNCH_INITIAL_REFRESH
zurückgeben. Dadurch führtRemoteMediator
eine Remote-Aktualisierung durch, um die Daten vollständig neu zu laden. Alle Remote-LadevorgängeAPPEND
oderPREPEND
warten, bis derREFRESH
-Ladevorgang abgeschlossen ist, bevor er fortgesetzt wird. - Falls die lokalen Daten nicht aktualisiert werden müssen, sollte
initialize()
InitializeAction.SKIP_INITIAL_REFRESH
zurückgeben. Dadurch überspringtRemoteMediator
die Remote-Aktualisierung und lädt die im Cache gespeicherten Daten.
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
Schließlich müssen Sie eine Pager
-Instanz erstellen, um den Stream von ausgelagerten Daten einzurichten.
Dies ähnelt dem Erstellen einer Pager
aus einer einfachen Netzwerkdatenquelle. Dabei müssen Sie jedoch zwei Schritte unterschiedlich ausführen:
- Anstatt einen
PagingSource
-Konstruktor direkt zu übergeben, müssen Sie die Abfragemethode angeben, die einPagingSource
-Objekt aus dem DAO zurückgibt. - Sie müssen eine Instanz Ihrer
RemoteMediator
-Implementierung alsremoteMediator
-Parameter angeben.
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, mit der Ihre Anwendung beim Laden von Daten aus mehreren Quellen umgehen muss, tritt auf, wenn lokale im Cache gespeicherte Daten mit der Remote-Datenquelle nicht mehr synchron sind.
Wenn die Methode initialize()
Ihrer RemoteMediator
-Implementierung LAUNCH_INITIAL_REFRESH
zurückgibt, sind die Daten veraltet und müssen durch neue Daten ersetzt werden. Alle PREPEND
- oder APPEND
-Ladeanfragen müssen warten, bis der Remote-Ladevorgang REFRESH
abgeschlossen ist. Da die PREPEND
- oder APPEND
-Anfragen vor der REFRESH
-Anfrage in die Warteschlange gestellt wurden, kann es sein, dass die PagingState
-Anfragen, die an diese Ladeaufrufe übergeben wurden, zum Zeitpunkt ihrer Ausführung veraltet sind.
Je nachdem, wie die Daten lokal gespeichert werden, kann Ihre Anwendung redundante Anfragen ignorieren, wenn Änderungen an den im Cache gespeicherten Daten zu einer Entwertung und neuen Datenabrufen führen.
Beispiel: „Room“ macht Abfragen für Dateneinfügungen ungültig. Das bedeutet, dass neue PagingSource
-Objekte mit den aktualisierten Daten für ausstehende Ladeanfragen bereitgestellt werden, wenn neue Daten in die Datenbank eingefügt werden.
Die Lösung dieses Problems bei der Datensynchronisierung ist wichtig, damit Nutzer die relevantesten und aktuellsten Daten sehen. Die beste Lösung hängt vor allem davon ab, wie die Netzwerkdatenquelle die Daten seitent. Mit Remote-Schlüsseln können Sie in jedem Fall Informationen über die zuletzt vom Server angeforderte Seite speichern. Anhand dieser Informationen kann die Anwendung die richtige Seite mit Daten, die als Nächstes geladen werden soll, ermitteln und anfordern.
Remote-Schlüssel verwalten
Remote-Schlüssel sind Schlüssel, mit denen eine RemoteMediator
-Implementierung dem Back-End-Dienst mitteilt, welche Daten als Nächstes geladen werden sollen. Im einfachsten Fall enthält jedes Element mit ausgelagerten Daten einen Remoteschlüssel, auf den Sie einfach verweisen können. Wenn die Remoteschlüssel jedoch nicht einzelnen Elementen entsprechen, müssen Sie sie separat speichern und in der Methode load()
verwalten.
In diesem Abschnitt wird beschrieben, wie Sie Remote-Schlüssel erfassen, speichern und aktualisieren, die nicht in einzelnen Elementen gespeichert sind.
Elementschlüssel
In diesem Abschnitt wird beschrieben, wie Sie mit Remoteschlüsseln arbeiten, die einzelnen Elementen entsprechen. Wenn ein API-Schlüssel für einzelne Elemente verwendet wird, wird normalerweise die Element-ID als Abfrageparameter übergeben. Der Parametername gibt an, ob der Server mit Elementen vor oder nach der angegebenen ID antworten soll. Im Beispiel der Modellklasse User
wird das Feld id
des Servers als Remoteschlüssel verwendet, wenn zusätzliche Daten angefordert werden.
Wenn mit der Methode load()
artikelspezifische Remote-Schlüssel verwaltet werden müssen, sind diese Schlüssel in der Regel die IDs der vom Server abgerufenen Daten. Aktualisierungsvorgänge benötigen keinen Ladeschlüssel, da sie nur die neuesten Daten abrufen.
Ebenso müssen bei vorangehenden Vorgängen keine zusätzlichen Daten abgerufen werden, da bei der Aktualisierung immer die neuesten Daten vom Server abgerufen werden.
Für Anfügevorgänge ist jedoch eine ID erforderlich. Dazu müssen Sie das letzte Element aus der Datenbank laden und seine ID verwenden, um die nächste Seite mit Daten zu laden. Wenn die Datenbank keine Elemente enthält, wird endOfPaginationReached
auf „true“ gesetzt und weist darauf hin, dass eine Datenaktualisierung erforderlich ist.
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); }
Seitenschlüssel
In diesem Abschnitt wird beschrieben, wie Sie mit Remoteschlüsseln arbeiten, die nicht einzelnen Elementen entsprechen.
Tabelle mit Remote-Schlüsseln hinzufügen
Wenn Remote-Schlüssel nicht direkt mit Listenelementen verknüpft sind, empfiehlt es sich, sie in einer separaten Tabelle in der lokalen Datenbank zu speichern. Definieren Sie eine „Room“-Entität, die eine Tabelle mit Remote-Schlüsseln darstellt:
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; }
Außerdem müssen Sie einen DAO für die RemoteKey
-Entität 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 die Methode load()
Remote-Seitenschlüssel verwalten muss, müssen Sie sie im Vergleich zur grundlegenden Verwendung von RemoteMediator
auf folgende Weise anders definieren:
- Fügen Sie ein zusätzliches Attribut hinzu, das einen Verweis auf die DAO für Ihre Remote-Schlüsseltabelle enthält.
- Bestimmen Sie, welcher Schlüssel als Nächstes geladen werden soll. Fragen Sie dazu die Remote-Schlüsseltabelle ab, anstatt
PagingState
zu verwenden. - Fügen oder speichern Sie den zurückgegebenen Remote-Schlüssel aus der Netzwerkdatenquelle zusätzlich zu den ausgelagerten Daten 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 Anwendung wie in den vorherigen Beispielen nur Netzwerkaktualisierungen von ganz oben in der Liste unterstützen muss, muss RemoteMediator
kein vorangestelltes Ladeverhalten definieren.
Wenn Ihre Anwendung jedoch das inkrementelle Laden aus dem Netzwerk in die lokale Datenbank unterstützen muss, müssen Sie die Fortsetzung der Paginierung ab dem Anker, der Scrollposition des Nutzers, unterstützen. Die PagingSource
-Implementierung von Room übernimmt dies für Sie. Wenn Sie Room jedoch nicht verwenden, können Sie PagingSource.getRefreshKey()
überschreiben.
Eine Beispielimplementierung von getRefreshKey()
finden Sie unter PagingSource definieren.
Abbildung 4 zeigt den Vorgang, bei dem Daten zuerst aus der lokalen Datenbank und dann aus dem Netzwerk geladen werden, sobald die Datenbank keine Daten mehr hat.
Weitere Informationen
Weitere Informationen zur Paging-Bibliothek finden Sie in den folgenden Ressourcen:
Codelabs
Produktproben
Empfehlungen für dich
- Hinweis: Der Linktext wird angezeigt, wenn JavaScript deaktiviert ist.
- Auslagerungsdaten laden und anzeigen
- Auslagerungsimplementierung testen
- Migration zu Paging 3