Seite aus Netzwerk und Datenbank (Ansichten)

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

Abbildung 1 Diagramm des Lebenszyklus der Paging-Bibliothek mit PagingSource und PagingData.

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.

Abbildung 2 Diagramm des Lebenszyklus der Paging-Bibliothek mit PagingSource und RemoteMediator.

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.

Der RemoteMediator lädt Daten aus dem Netzwerk in die Datenbank und die PagingSource lädt Daten aus der Datenbank. Ein Pager verwendet sowohl den RemoteMediator als auch die PagingSource, um paginierte Daten zu laden.
Abbildung 3. Diagramm einer Paging-Implementierung, die eine mehrschichtige Daten quelle verwendet.

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 ein Pager-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:

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:

  1. Bestimmen Sie, welche Seite aus dem Netzwerk geladen werden soll, je nach Ladeart und den bisher geladenen Daten.
  2. Lösen Sie die Netzwerkanfrage aus.
  3. 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.Error zurück.

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ückgeben InitializeAction.LAUNCH_INITIAL_REFRESH. Dadurch führt der RemoteMediator eine Remote-Aktualisierung durch, um die Daten vollständig neu zu laden. Alle Remote-Ladevorgänge vom Typ APPEND oder PREPEND warten auf den erfolgreichen Abschluss des Ladevorgangs vom Typ REFRESH, bevor sie fortgesetzt werden.
  • Wenn die lokalen Daten nicht aktualisiert werden müssen, initialize() sollte zurückgeben InitializeAction.SKIP_INITIAL_REFRESH. Dadurch überspringt der RemoteMediator die 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 ein PagingSource-Objekt aus dem DAO zurückgibt.
  • Sie müssen eine Instanz Ihrer RemoteMediator-Implementierung als remoteMediator-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 PagingState zu 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