Konzepte und Jetpack Compose-Implementierung
Sorgen Sie für eine bessere Nutzererfahrung, 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 Seiten aus dem Netzwerk und aus einer lokalen Datenbank zu laden. Auf diese Weise ruft Ihre App die UI aus einem lokalen Datenbankcache ab und sendet nur dann Anfragen an das Netzwerk, wenn keine Daten mehr in der Datenbank vorhanden sind.
In diesem Leitfaden wird davon ausgegangen, dass Sie mit der Room-Persistenz bibliothek und der grundlegenden Verwendung der Paging bibliothek vertraut sind.
Datenladevorgänge koordinieren
Die Paging-Bibliothek bietet die
RemoteMediator Komponente
für diesen Anwendungsfall. RemoteMediator fungiert als Signal der Paging-Bibliothek, wenn der App die Daten im Cache ausgehen. Sie können dieses Signal verwenden, um
zusätzliche Daten aus dem Netzwerk zu laden und in der lokalen Datenbank zu speichern, wo sie von einer
PagingSource geladen und der UI zur Anzeige
bereitgestellt werden können.
Wenn zusätzliche Daten benötigt werden, ruft die Paging-Bibliothek die
load() Methode aus
der RemoteMediator Implementierung auf. Dies ist eine aussetzende Funktion, sodass lange Vorgänge sicher ausgeführt werden können. Diese Funktion ruft in der Regel die neuen Daten aus einer Netzwerkquelle ab und speichert sie im lokalen Speicher.
Dieser Vorgang funktioniert mit neuen Daten, aber im Laufe der Zeit müssen die in der Datenbank gespeicherten Daten ungültig gemacht werden, z. B. wenn der Nutzer manuell eine Aktualisierung auslöst. Dies
wird durch die LoadType
Eigenschaft dargestellt, die an die load() Methode übergeben wird. LoadType informiert den RemoteMediator darüber, ob die vorhandenen Daten aktualisiert oder zusätzliche Daten abgerufen werden müssen, die an die vorhandene Liste angehängt oder vorangestellt werden müssen.
Auf diese Weise sorgt der RemoteMediator dafür, dass Ihre App die Daten, die Nutzer sehen möchten, in der richtigen Reihenfolge lädt.
Lebenszyklus der Paging-Bibliothek
Wenn die Paging-Bibliothek direkt aus dem Netzwerk geladen wird, lädt die PagingSource die Daten und
gibt ein
LoadResult
Objekt zurück. Die PagingSource Implementierung wird über den
Pager Parameter an den
pagingSourceFactory übergeben.
Wenn die UI neue Daten benötigt, ruft der Pager die
load() Methode aus der
PagingSource auf und gibt einen Stream von
PagingData Objekten zurück, die
die neuen Daten kapseln. Jedes PagingData-Objekt wird in der Regel im ViewModel im Cache gespeichert, bevor es zur Anzeige an die UI gesendet wird.
RemoteMediator ändert diesen Datenfluss. Eine PagingSource lädt weiterhin die Daten. Wenn die Seiten jedoch erschöpft sind, löst die Paging-Bibliothek den RemoteMediator aus, um neue Daten aus der Netzwerkquelle zu laden. Der RemoteMediator speichert die neuen Daten in der lokalen Datenbank, sodass ein In-Memory-Cache im ViewModel nicht erforderlich ist. Schließlich macht sich die PagingSource ungültig und der Pager erstellt eine neue Instanz, um die neuen Daten aus der Datenbank zu laden.
Grundlegende Nutzung
Angenommen, Ihre App soll Seiten mit User-Elementen aus einer Netzwerkdatenquelle mit Element-Schlüsseln in einen lokalen Cache laden, der in einer Room-Datenbank gespeichert ist.
Eine RemoteMediator-Implementierung hilft, Seiten mit Daten aus dem Netzwerk in die Datenbank zu laden, lädt aber keine Daten direkt in die UI. Stattdessen verwendet die App
die Datenbank als die Quelle der
Wahrheit. Mit anderen Worten: Die App zeigt nur Daten an, die in der Datenbank im Cache gespeichert wurden. Eine PagingSource-Implementierung (z. B. eine von Room generierte) lädt die im Cache gespeicherten Daten aus der Datenbank in die UI.
Room-Entitäten erstellen
Im ersten Schritt definieren Sie mit der Room-Persistenz
bibliothek eine Datenbank, die einen
lokalen Cache mit Seiten mit Daten aus der Netzwerkdatenquelle enthält. Beginnen Sie mit einer
Implementierung von RoomDatabase
, wie unter Daten in einer lokalen Datenbank mit
Room speichern beschrieben.
Definieren Sie als Nächstes eine Room-Entität, die eine Tabelle mit Listenelementen darstellt, wie unter
Daten mit Room-Entitäten definieren beschrieben.
Geben Sie ein id-Feld als Primärschlüssel sowie Felder für alle anderen Informationen an, die Ihre Listenelemente enthalten.
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 Data Access Object (DAO) für diese Room-Entität definieren, wie unter Mit Room DAOs auf Daten zugreifen beschrieben. Das DAO für die Listenelement-Entitä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. Auf diese Weise kann einPager-Objekt diese Tabelle als Quelle für Seiten mit Daten verwenden. - Eine
clearAll()-Methode, mit der alle Daten der Tabelle gelöscht werden.
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 Hauptaufgabe von RemoteMediator besteht darin, weitere Daten aus dem Netzwerk zu laden, wenn dem Pager die Daten ausgehen oder die vorhandenen Daten ungültig gemacht werden. Es enthält eine load()-Methode, die Sie überschreiben müssen, um das Ladeverhalten zu definieren.
Eine typische RemoteMediator-Implementierung enthält die folgenden Parameter:
query: Ein Abfragestring, der definiert, welche Daten aus dem Back-End-Dienst abgerufen werden sollen.database: Die Room-Datenbank, die als lokaler Cache dient.networkService: Eine API-Instanz für den Backend-Dienst.
Erstellen Sie eine RemoteMediator<Key, Value>-Implementierung. Die Typen Key und Value sollten dieselben sein wie bei der Definition einer PagingSource für dieselbe Netzwerkdatenquelle. Weitere Informationen zum
Auswählen von Typparametern finden Sie unter Schlüssel- und Wert
typenauswählen.
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 das Aktualisieren des Sicherungsdatensatzes und das Ungültigmachen der PagingSource verantwortlich. Einige Bibliotheken, die die Paging-Bibliothek unterstützen (z. B. Room), machen die von ihnen implementierten PagingSource-Objekte automatisch ungültig.
Die Methode load() verwendet zwei Parameter:
PagingStateenthält Informationen zu den bisher geladenen Seiten, dem zuletzt aufgerufenen Index und demPagingConfig-Objekt, mit dem Sie den Paging-Stream initialisiert haben.LoadTypegibt den Typ des Ladevorgangs an:REFRESH,APPENDoderPREPEND.
Der Rückgabewert der load() Methode ist ein
MediatorResult
Objekt. MediatorResult kann entweder
MediatorResult.Error
(mit der Fehlerbeschreibung) oder
MediatorResult.Success
(mit einem Signal, das angibt, ob weitere Daten geladen werden müssen) sein.
Die Methode load() muss die folgenden Schritte ausführen:
- Bestimmen Sie, welche Seite aus dem Netzwerk geladen werden soll, je nach Ladeart und den bisher geladenen Daten.
- Lösen Sie die Netzwerkanfrage aus.
- Führen Sie je nach Ergebnis des Ladevorgangs Aktionen aus:
- Wenn der Ladevorgang erfolgreich ist und die empfangene Liste von Elementen nicht leer ist, speichern Sie die Listenelemente in der Datenbank und geben Sie
MediatorResult.Success(endOfPaginationReached = false)zurück. Nachdem die Daten gespeichert wurden, machen Sie die Datenquelle ungültig, um die Paging-Bibliothek über die neuen Daten zu informieren. - Wenn der Ladevorgang erfolgreich ist und die empfangene Liste von Elementen leer ist oder es sich um den letzten Seitenindex handelt, geben Sie
MediatorResult.Success(endOfPaginationReached = true)zurück. Nachdem die Daten gespeichert wurden, machen Sie die Datenquelle ungültig, um die Paging-Bibliothek über die neuen Daten zu informieren. - Wenn die Anfrage einen Fehler verursacht, geben Sie
MediatorResult.Errorzurück.
- Wenn der Ladevorgang erfolgreich ist und die empfangene Liste von Elementen nicht leer ist, speichern Sie die Listenelemente in der Datenbank und geben Sie
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 ); }
Methode „initialize“ definieren
RemoteMediator-Implementierungen können auch die
initialize()
Methode überschreiben, um zu prüfen, ob die Daten im Cache veraltet sind, und zu entscheiden, ob
eine Remote-Aktualisierung ausgelöst werden soll. Diese Methode wird ausgeführt, bevor ein Ladevorgang durchgeführt wird. So können Sie die Datenbank bearbeiten (z. B. alte Daten löschen), bevor Sie lokale oder Remote-Ladevorgänge auslösen.
Da initialize() eine asynchrone Funktion ist, können Sie Daten laden, um die Relevanz der vorhandenen Daten in der Datenbank zu bestimmen. In den meisten Fällen sind die Daten im Cache nur für einen bestimmten Zeitraum gültig. Der RemoteMediator kann prüfen, ob dieser Ablaufzeitpunkt überschritten wurde. In diesem Fall muss die Paging-Bibliothek die Daten vollständig aktualisieren. Implementierungen von initialize() sollten eine InitializeAction wie folgt zurückgeben:
- Wenn die lokalen Daten vollständig aktualisiert werden müssen,
initialize()sollte zurückgebenInitializeAction.LAUNCH_INITIAL_REFRESH. Dadurch führt derRemoteMediatoreine Remote-Aktualisierung durch, um die Daten vollständig neu zu laden. Alle Remote-Ladevorgänge vom TypAPPENDoderPREPENDwarten auf den erfolgreichen Abschluss des Ladevorgangs vom TypREFRESH, bevor sie fortgesetzt werden. - Wenn die lokalen Daten nicht aktualisiert werden müssen,
initialize()sollte zurückgebenInitializeAction.SKIP_INITIAL_REFRESH. Dadurch überspringt derRemoteMediatordie Remote-Aktualisierung und lädt die Daten im Cache.
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 mit Seiten mit Daten einzurichten.
Dies ähnelt dem Erstellen eines Pager aus einer einfachen Netzwerkdatenquelle, aber es gibt zwei Dinge, die Sie anders machen müssen:
- 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.
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));
Fernbedienungen verwalten
Remote-Schlüssel sind Schlüssel, die eine RemoteMediator-Implementierung verwendet, um dem Backend-Dienst mitzuteilen, welche Daten als Nächstes geladen werden sollen. Im einfachsten Fall enthält jedes Element mit Seiten mit Daten einen Remote-Schlüssel, auf den Sie einfach verweisen können. Wenn die Remote-Schlüssel jedoch nicht einzelnen Elementen entsprechen, müssen Sie sie separat speichern und in Ihrer Methode load() verwalten.
In diesem Abschnitt wird beschrieben, wie Sie Remote-Schlüssel erfassen, speichern und aktualisieren, die nicht in einzelnen Elementen gespeichert sind.
Element-Schlüssel
In diesem Abschnitt wird beschrieben, wie Sie mit Remote-Schlüsseln arbeiten, die einzelnen Elementen entsprechen. Wenn eine API Schlüssel für einzelne Elemente verwendet, wird die Element-ID in der Regel als Abfrageparameter übergeben. Der Parametername gibt an, ob der Server mit Elementen vor oder nach der angegebenen ID antworten soll. Im Beispiel der User-Modellklasse wird das Feld id vom Server als Remote-Schlüssel verwendet, wenn zusätzliche Daten angefordert werden.
Wenn Ihre Methode load() elementspezifische Remote-Schlüssel verwalten muss, sind diese Schlüssel in der Regel die IDs der vom Server abgerufenen Daten. Für Aktualisierungsvorgänge ist kein Ladeschlüssel erforderlich, da nur die neuesten Daten abgerufen werden.
Ebenso müssen bei Vorgängen vom Typ „prepend“ keine zusätzlichen Daten abgerufen werden, da bei der Aktualisierung immer die neuesten Daten vom Server abgerufen werden.
Für Vorgänge vom Typ „append“ 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 keine Elemente in der Datenbank vorhanden sind, wird endOfPaginationReached auf „true“ gesetzt, was darauf hinweist, dass eine Datenaktualisierung erforderlich ist.
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); }
Seiten-Schlüssel
In diesem Abschnitt wird beschrieben, wie Sie mit Remote-Schlü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, sollten Sie sie in einer separaten Tabelle in der lokalen Datenbank speichern. Definieren Sie eine Room-Entität, die eine Tabelle mit Remote-Schlüsseln darstellt:
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 ein DAO für die RemoteKey-Entität definieren:
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-Schlüsseln laden
Wenn Ihre load() Methode Remote-Seitenschlüssel verwalten muss, müssen Sie sie im Vergleich zur grundlegenden Verwendung von RemoteMediator anders definieren:
- Fügen Sie eine zusätzliche Eigenschaft hinzu, die einen Verweis auf das DAO für Ihre Tabelle mit Remote-Schlüsseln enthält.
- Bestimmen Sie, welcher Schlüssel als Nächstes geladen werden soll, indem Sie die Tabelle mit Remote-Schlüsseln abfragen, anstatt
PagingStatezu verwenden. - Fügen Sie den zurückgegebenen Remote-Schlüssel aus der Netzwerkdatenquelle zusätzlich zu den Seiten mit Daten selbst ein oder speichern Sie ihn.
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); }
Zusätzliche Ressourcen
Weitere Informationen zur Paging-Bibliothek finden Sie in den folgenden zusätzlichen Ressourcen:
Codelabs
Produktproben
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- Seiten mit Daten laden und anzeigen
- Paging-Implementierung testen
- Zu Paging 3 migrieren