Seite aus Netzwerk und Datenbank

Nutzerfreundlichkeit verbessern, indem du dafür sorgst, dass deine App verwendet werden kann wenn die Netzwerkverbindungen unzuverlässig sind oder der Nutzer offline ist. Eine Möglichkeit, um gleichzeitig vom Netzwerk und von einer lokalen Datenbank aus zu suchen. Auf diese Weise bedient Ihre Anwendung die UI aus einem lokalen Datenbank-Cache und an das Netzwerk senden, wenn keine Daten mehr in der Datenbank vorhanden sind.

In diesem Leitfaden wird davon ausgegangen, dass Sie mit der Funktion Raumpersistenz vertraut sind. und mit der grundlegenden Verwendung der Paging-Funktion Mediathek.

Koordinatenladevorgänge

Die Paging-Bibliothek bietet die RemoteMediator-Komponente für diesen Anwendungsfall. RemoteMediator fungiert als Signal von der Paging-Bibliothek Wenn die App keine im Cache gespeicherten Daten mehr hat. Mit diesem Signal können Sie zusätzliche Daten aus dem Netzwerk abrufen und in der lokalen Datenbank speichern, PagingSource kann sie laden und der Benutzeroberfläche zum Anzeigen bereitgestellt werden.

Wenn zusätzliche Daten benötigt werden, ruft die Paging Library die Methode load() von RemoteMediator-Implementierung verwenden. Dies ist eine aussetzende Funktion, daher ist sie sicher lang andauernde Arbeit durchzuführen. Diese Funktion ruft in der Regel die neuen Daten aus einer Netzwerkquelle und speichert sie im lokalen Speicher.

Dieser Prozess funktioniert mit neuen Daten, aber im Laufe der Zeit werden die in der Datenbank gespeicherten Daten erfordert eine Entwertung, z. B. wenn der Nutzer manuell eine Aktualisierung auslöst. Dieses wird durch LoadType repräsentiert -Eigenschaft an die load()-Methode übergeben. Das LoadType informiert die RemoteMediator, ob die vorhandenen Daten aktualisiert oder zusätzliche Daten, die der vorhandenen Liste hinzugefügt oder vorangestellt werden müssen.

Auf diese Weise sorgt das RemoteMediator dafür, dass Ihre App die Daten lädt, die die Nutzende in der richtigen Reihenfolge sehen möchten.

Paging-Lebenszyklus

<ph type="x-smartling-placeholder">
</ph>
<ph type="x-smartling-placeholder">
</ph> Abbildung 1: Diagramm des Lebenszyklus von Paging mit PagingSource und PagingData.

Beim direkten Paging aus dem Netzwerk lädt PagingSource die Daten und gibt Folgendes zurück: LoadResult -Objekt enthält. Die Implementierung PagingSource wird an den Pager über die pagingSourceFactory.

Da für die Benutzeroberfläche neue Daten benötigt werden, ruft Pager die Methode Methode load() aus dem PagingSource und gibt einen Stream von PagingData-Objekte, die die neuen Daten zu kapseln. Jedes PagingData-Objekt wird normalerweise im ViewModel, bevor sie zur Anzeige an die UI gesendet werden.

<ph type="x-smartling-placeholder">
</ph>
<ph type="x-smartling-placeholder">
</ph> Abbildung 2: Diagramm des Lebenszyklus von Paging mit PagingSource und RemoteMediator.

RemoteMediator ändert diesen Datenfluss. Ein PagingSource lädt die Daten trotzdem. Sind die Seitendaten jedoch erschöpft, löst die Paging-Bibliothek RemoteMediator, um neue Daten aus der Netzwerkquelle zu laden. Das RemoteMediator speichert die neuen Daten in der lokalen Datenbank, also ein speicherinterner Cache in der ViewModel ist nicht erforderlich. Schließlich ungültigt sich PagingSource selbst und Der Pager erstellt eine neue Instanz, um die neuen Daten aus der Datenbank zu laden.

Grundlegende Nutzung

Angenommen, Ihre App soll Seiten von User Elementen aus einem mit Artikeln zusammenhängenden Netzwerkdatenquelle in einen lokalen Cache, der in einer Raumdatenbank gespeichert ist.

<ph type="x-smartling-placeholder">
</ph> Der RemoteMediator lädt Daten aus dem Netzwerk in die Datenbank
    lädt die PagingSource Daten aus der Datenbank. Ein Pager verwendet sowohl das
    RemoteMediator und PagingSource.
<ph type="x-smartling-placeholder">
</ph> Abbildung 3: Diagramm einer Paging-Implementierung mit mehrschichtigen Daten Quelle.

Mit einer RemoteMediator-Implementierung können Seitendaten aus dem Netzwerk in in der Datenbank gespeichert, Daten werden jedoch nicht direkt in die Benutzeroberfläche geladen. Stattdessen verwendet die App die Datenbank als Quelle Wahrheit. Mit anderen Worten: Die App zeigt Daten an, die in der Datenbank zwischengespeichert wurden. Ein PagingSource Eine Implementierung (z. B. eine von Room generierte) verarbeitet das Laden von Daten aus dem Cache. aus der Datenbank in die Benutzeroberfläche.

Elemente des Chatrooms erstellen

Der erste Schritt besteht darin, mit der Funktion Raumpersistenz Bibliothek zum Definieren einer Datenbank, lokalen Cache für Paged-Daten aus der Netzwerkdatenquelle. Beginnen Sie mit einer Implementierung von RoomDatabase wie unter Speichern von Daten in einer lokalen Datenbank mithilfe Chatroom.

Definieren Sie als Nächstes eine Raumentität zur Darstellung einer Tabelle mit Listenelementen, wie in Daten mithilfe von „Room“-Entitäten definieren Weisen Sie ihr das Feld id als Primärschlüssel sowie Felder für andere Informationen, die Ihre Listenelemente enthalten.

Kotlin

@Entity(tableName = "users")
data class User(val id: String, val label: String)

Java

@Entity(tableName = "users")
public class User {
  public String id;
  public String label;
}

Java

@Entity(tableName = "users")
public class User {
  public String id;
  public String label;
}

Sie müssen auch ein Datenzugriffsobjekt (Data Access Object, DAO) für diese Raumentität als wie unter Zugriff auf Daten über die Funktion DAOs Der DAO für das Listenelement Entität muss die folgenden Methoden enthalten:

  • Eine insertAll()-Methode, die eine Liste von Elementen in die Tabelle einfügt.
  • Eine Methode, die den Abfragestring als Parameter verwendet und einen PagingSource-Objekt für die Ergebnisliste. Auf diese Weise kann ein Pager-Objekt können Sie diese Tabelle als Quelle für die Seitendaten verwenden.
  • Eine clearAll()-Methode, mit der alle Daten der Tabelle gelöscht werden.

Kotlin

@Dao
interface UserDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insertAll(users: List<User>)

  @Query("SELECT * FROM users WHERE label LIKE :query")
  fun pagingSource(query: String): PagingSource<Int, User>

  @Query("DELETE FROM users")
  suspend fun clearAll()
}

Java

@Dao
interface UserDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertAll(List<User> users);

  @Query("SELECT * FROM users WHERE mLabel LIKE :query")
  PagingSource<Integer, User> pagingSource(String query);

  @Query("DELETE FROM users")
  int clearAll();
}

Java

@Dao
interface UserDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertAll(List<User> users);

  @Query("SELECT * FROM users WHERE mLabel LIKE :query")
  PagingSource<Integer, User> pagingSource(String query);

  @Query("DELETE FROM users")
  int clearAll();
}

RemoteMediator implementieren

Die Hauptfunktion von RemoteMediator besteht darin, mehr Daten aus dem Netzwerk zu laden, Entweder sind für Pager die Daten aufgebraucht oder die vorhandenen Daten wurden ungültig gemacht. Es enthält eine load()-Methode, die Sie überschreiben müssen, um das Laden verhalten.

Eine typische RemoteMediator-Implementierung enthält die folgenden Parameter:

  • query: Ein Abfragestring, mit dem definiert wird, welche Daten aus dem Back-End abgerufen werden sollen. .
  • database: Die Raumdatenbank, die als lokaler Cache dient.
  • networkService: Eine API-Instanz für den Back-End-Dienst.

Erstellen Sie eine RemoteMediator<Key, Value>-Implementierung. Der Typ Key und der Value-Typ muss mit dem gleichen Code übereinstimmen wie bei der Definition einer PagingSource für dieselbe Netzwerkdatenquelle. Weitere Informationen zu Typparameter auswählen, siehe Schlüssel und Wert auswählen .

Kotlin

@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
  private val query: String,
  private val database: RoomDb,
  private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
  val userDao = database.userDao()

  override suspend fun load(
    loadType: LoadType,
    state: PagingState<Int, User>
  ): MediatorResult {
    // ...
  }
}

Java

@UseExperimental(markerClass = ExperimentalPagingApi.class)
class ExampleRemoteMediator extends RxRemoteMediator<Integer, User> {
  private String query;
  private ExampleBackendService networkService;
  private RoomDb database;
  private UserDao userDao;

  ExampleRemoteMediator(
    String query,
    ExampleBackendService networkService, RoomDb database
  ) {
    query = query;
    networkService = networkService;
    database = database;
    userDao = database.userDao();
  }

  @NotNull
  @Override
  public Single<MediatorResult> loadSingle(
    @NotNull LoadType loadType,
    @NotNull PagingState<Integer, User> state
  ) {
    ...
  }
}

Java

class ExampleRemoteMediator extends ListenableFutureRemoteMediator<Integer, User> {
  private String query;
  private ExampleBackendService networkService;
  private RoomDb database;
  private UserDao userDao;
  private Executor bgExecutor;

  ExampleRemoteMediator(
    String query,
    ExampleBackendService networkService,
    RoomDb database,
    Executor bgExecutor
  ) {
    this.query = query;
    this.networkService = networkService;
    this.database = database;
    this.userDao = database.userDao();
    this.bgExecutor = bgExecutor;
  }

  @NotNull
  @Override
  public ListenableFuture<MediatorResult> loadFuture(
    @NotNull LoadType loadType,
    @NotNull PagingState<Integer, User> state
  ) {
    ...
  }
}

Die Methode load() ist für die Aktualisierung des unterstützenden Datasets und PagingSource wird ungültig. Einige Bibliotheken, die Paging unterstützen (z. B. Room) verarbeitet automatisch die Entwertung von PagingSource-Objekten, umsetzen.

Die Methode load() akzeptiert zwei Parameter:

Der Rückgabewert der Methode load() ist ein MediatorResult -Objekt enthält. MediatorResult kann entweder MediatorResult.Error (einschließlich Fehlerbeschreibung) oder MediatorResult.Success (dazu gehört ein Signal, das angibt, ob mehr Daten geladen werden müssen)

Die Methode load() muss die folgenden Schritte ausführen:

  1. Bestimmen Sie je nach Ladetyp und die bisher geladen wurden.
  2. Netzwerkanfrage auslösen
  3. Führen Sie je nach Ergebnis des Ladevorgangs Aktionen aus: <ph type="x-smartling-placeholder">
      </ph>
    • Wenn der Ladevorgang erfolgreich ist und die Liste der empfangenen Artikel nicht leer ist, speichern Sie die Listenelemente in der Datenbank MediatorResult.Success(endOfPaginationReached = false) Nach den Daten gespeichert ist, entwerten Sie die Datenquelle, um die Paging-Bibliothek neuen Daten.
    • Wenn der Ladevorgang erfolgreich ist und entweder die Liste der empfangenen Elemente leer ist oder den Index der letzten Seite. MediatorResult.Success(endOfPaginationReached = true) Nachdem die Daten gespeichert haben, entwerten Sie die Datenquelle, um die Paging-Bibliothek über das neue Daten.
    • Wenn die Anfrage einen Fehler verursacht, wird MediatorResult.Error zurückgegeben.

Kotlin

override suspend fun load(
  loadType: LoadType,
  state: PagingState<Int, User>
): MediatorResult {
  return try {
    // The network load method takes an optional after=<user.id>
    // parameter. For every page after the first, pass the last user
    // ID to let it continue from where it left off. For REFRESH,
    // pass null to load the first page.
    val loadKey = when (loadType) {
      LoadType.REFRESH -> null
      // In this example, you never need to prepend, since REFRESH
      // will always load the first page in the list. Immediately
      // return, reporting end of pagination.
      LoadType.PREPEND ->
        return MediatorResult.Success(endOfPaginationReached = true)
      LoadType.APPEND -> {
        val lastItem = state.lastItemOrNull()

        // You must explicitly check if the last item is null when
        // appending, since passing null to networkService is only
        // valid for initial load. If lastItem is null it means no
        // items were loaded after the initial REFRESH and there are
        // no more items to load.
        if (lastItem == null) {
          return MediatorResult.Success(
            endOfPaginationReached = true
          )
        }

        lastItem.id
      }
    }

    // Suspending network load via Retrofit. This doesn't need to be
    // wrapped in a withContext(Dispatcher.IO) { ... } block since
    // Retrofit's Coroutine CallAdapter dispatches on a worker
    // thread.
    val response = networkService.searchUsers(
      query = query, after = loadKey
    )

    database.withTransaction {
      if (loadType == LoadType.REFRESH) {
        userDao.deleteByQuery(query)
      }

      // Insert new users into database, which invalidates the
      // current PagingData, allowing Paging to present the updates
      // in the DB.
      userDao.insertAll(response.users)
    }

    MediatorResult.Success(
      endOfPaginationReached = response.nextKey == null
    )
  } catch (e: IOException) {
    MediatorResult.Error(e)
  } catch (e: HttpException) {
    MediatorResult.Error(e)
  }
}

Java

@NotNull
@Override
public Single<MediatorResult> loadSingle(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional after=<user.id> parameter. For
  // every page after the first, pass the last user ID to let it continue from
  // where it left off. For REFRESH, pass null to load the first page.
  String loadKey = null;
  switch (loadType) {
    case REFRESH:
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Single.just(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when appending,
      // since passing null to networkService is only valid for initial load.
      // If lastItem is null it means no items were loaded after the initial
      // REFRESH and there are no more items to load.
      if (lastItem == null) {
        return Single.just(new MediatorResult.Success(true));
      }

      loadKey = lastItem.getId();
      break;
  }

  return networkService.searchUsers(query, loadKey)
    .subscribeOn(Schedulers.io())
    .map((Function<SearchUserResponse, MediatorResult>) response -> {
      database.runInTransaction(() -> {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query);
        }

        // Insert new users into database, which invalidates the current
        // PagingData, allowing Paging to present the updates in the DB.
        userDao.insertAll(response.getUsers());
      });

      return new MediatorResult.Success(response.getNextKey() == null);
    })
    .onErrorResumeNext(e -> {
      if (e instanceof IOException || e instanceof HttpException) {
        return Single.just(new MediatorResult.Error(e));
      }

      return Single.error(e);
    });
}

Java

@NotNull
@Override
public ListenableFuture<MediatorResult> loadFuture(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional after=<user.id> parameter. For
  // every page after the first, pass the last user ID to let it continue from
  // where it left off. For REFRESH, pass null to load the first page.
  String loadKey = null;
  switch (loadType) {
    case REFRESH:
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Futures.immediateFuture(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when appending,
      // since passing null to networkService is only valid for initial load.
      // If lastItem is null it means no items were loaded after the initial
      // REFRESH and there are no more items to load.
      if (lastItem == null) {
        return Futures.immediateFuture(new MediatorResult.Success(true));
      }

      loadKey = lastItem.getId();
      break;
  }

  ListenableFuture<MediatorResult> networkResult = Futures.transform(
    networkService.searchUsers(query, loadKey),
    response -> {
      database.runInTransaction(() -> {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query);
        }

        // Insert new users into database, which invalidates the current
        // PagingData, allowing Paging to present the updates in the DB.
        userDao.insertAll(response.getUsers());
      });

      return new MediatorResult.Success(response.getNextKey() == null);
    }, bgExecutor);

  ListenableFuture<MediatorResult> ioCatchingNetworkResult =
    Futures.catching(
      networkResult,
      IOException.class,
      MediatorResult.Error::new,
      bgExecutor
    );

  return Futures.catching(
    ioCatchingNetworkResult,
    HttpException.class,
    MediatorResult.Error::new,
    bgExecutor
  );
}

Initialisierungsmethode definieren

RemoteMediator-Implementierungen können auch den initialize() um zu prüfen, ob im Cache gespeicherte Daten veraltet sind, und zu entscheiden, ob sie ausgelöst werden soll oder nicht. eine Remote-Aktualisierung durchführen. Diese Methode wird vor dem Laden ausgeführt, Manipulieren der Datenbank (z. B. um alte Daten zu löschen), bevor lokale oder Remote-Ladevorgänge.

Da initialize() eine asynchrone Funktion ist, können Sie Daten in die Relevanz der in der Datenbank vorhandenen Daten zu bestimmen. Am häufigsten Daten im Cache sind nur für einen bestimmten Zeitraum gültig. Die RemoteMediator kann prüfen, ob diese Ablaufzeit verstrichen ist. falls die Paging-Bibliothek die Daten vollständig aktualisieren muss. Implementierungen von initialize() sollte so eine InitializeAction zurückgeben:

  • Wenn die lokalen Daten vollständig aktualisiert werden müssen, initialize() sollte Folgendes zurückgeben: InitializeAction.LAUNCH_INITIAL_REFRESH Dies führt dazu, dass RemoteMediator eine Remote-Aktualisierung durchführt, um vollständig neu geladen zu werden. mit den Daten. Alle Remote-APPEND- oder PREPEND-Ladevorgänge warten auf das Laden von REFRESH. um erfolgreich zu sein, bevor Sie fortfahren.
  • Falls die lokalen Daten nicht aktualisiert werden müssen, initialize() sollte Folgendes zurückgeben: InitializeAction.SKIP_INITIAL_REFRESH Dadurch überspringt RemoteMediator die Remote-Aktualisierung und lädt die Daten im Cache gespeichert.

Kotlin

override suspend fun initialize(): InitializeAction {
  val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
  return if (System.currentTimeMillis() - db.lastUpdated() <= cacheTimeout)
  {
    // Cached data is up-to-date, so there is no need to re-fetch
    // from the network.
    InitializeAction.SKIP_INITIAL_REFRESH
  } else {
    // Need to refresh cached data from network; returning
    // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's
    // APPEND and PREPEND from running until REFRESH succeeds.
    InitializeAction.LAUNCH_INITIAL_REFRESH
  }
}

Java

@NotNull
@Override
public Single<InitializeAction> initializeSingle() {
  long cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
  return mUserDao.lastUpdatedSingle()
    .map(lastUpdatedMillis -> {
      if (System.currentTimeMillis() - lastUpdatedMillis <= cacheTimeout) {
        // Cached data is up-to-date, so there is no need to re-fetch
        // from the network.
        return InitializeAction.SKIP_INITIAL_REFRESH;
      } else {
        // Need to refresh cached data from network; returning
        // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's
        // APPEND and PREPEND from running until REFRESH succeeds.
        return InitializeAction.LAUNCH_INITIAL_REFRESH;
      }
    });
}

Java

@NotNull
@Override
public ListenableFuture<InitializeAction> initializeFuture() {
  long cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
  return Futures.transform(
    mUserDao.lastUpdated(),
    lastUpdatedMillis -> {
      if (System.currentTimeMillis() - lastUpdatedMillis <= cacheTimeout) {
        // Cached data is up-to-date, so there is no need to re-fetch
        // from the network.
        return InitializeAction.SKIP_INITIAL_REFRESH;
      } else {
        // Need to refresh cached data from network; returning
        // LAUNCH_INITIAL_REFRESH here will also block RemoteMediator's
        // APPEND and PREPEND from running until REFRESH succeeds.
        return InitializeAction.LAUNCH_INITIAL_REFRESH;
      }
    },
    mBgExecutor);
}

Pager erstellen

Abschließend müssen Sie eine Pager-Instanz erstellen, um den Stream von Seitendaten einzurichten. Dies ähnelt dem Erstellen einer Pager aus einer einfachen Netzwerkdatenquelle, aber müssen Sie zwei Dinge anders machen:

  • Anstatt einen PagingSource-Konstruktor direkt zu übergeben, müssen Sie den , die ein PagingSource-Objekt aus dem DAO zurückgibt.
  • Sie müssen eine Instanz Ihrer RemoteMediator-Implementierung als das remoteMediator-Parameter.

Kotlin

val userDao = database.userDao()
val pager = Pager(
  config = PagingConfig(pageSize = 50)
  remoteMediator = ExampleRemoteMediator(query, database, networkService)
) {
  userDao.pagingSource(query)
}

Java

UserDao userDao = database.userDao();
Pager<Integer, User> pager = Pager(
  new PagingConfig(/* pageSize = */ 20),
  null, // initialKey,
  new ExampleRemoteMediator(query, database, networkService)
  () -> userDao.pagingSource(query));

Java

UserDao userDao = database.userDao();
Pager<Integer, User> pager = Pager(
  new PagingConfig(/* pageSize = */ 20),
  null, // initialKey
  new ExampleRemoteMediator(query, database, networkService, bgExecutor),
  () -> userDao.pagingSource(query));

Race-Bedingungen verarbeiten

Eine Situation, die Ihre App bewältigen muss, wenn Sie Daten aus mehreren sind lokal im Cache gespeicherte Daten nicht mehr synchron mit dem Remote-Datenquelle.

Wenn die Methode initialize() aus Ihrer RemoteMediator-Implementierung Folgendes zurückgibt: LAUNCH_INITIAL_REFRESH, die Daten sind veraltet und müssen durch neue ersetzt werden Daten. Alle PREPEND- oder APPEND-Ladeanfragen werden gezwungen, auf die Remote- REFRESH geladen, um erfolgreich zu sein. Weil die PREPEND- oder APPEND-Anfragen vor der REFRESH-Anfrage in die Warteschlange gestellt wurde, ist es möglich, dass PagingState an diese Ladeaufrufe übergeben wurden, sind zum Zeitpunkt ihrer Ausführung veraltet.

Je nachdem, wie die Daten lokal gespeichert werden, kann Ihre App redundante -Anfragen, wenn Änderungen an den im Cache gespeicherten Daten zur Entwertung und neuen Datenabrufen führen. So werden z. B. Abfragen für eingefügte Daten ungültig. Das bedeutet, dass neue PagingSource-Objekte mit den aktualisierten Daten werden für den ausstehenden Ladevorgang bereitgestellt -Anfragen, wenn neue Daten in die Datenbank eingefügt werden.

Die Lösung dieses Datensynchronisierungsproblems ist unerlässlich, um sicherzustellen, die relevantesten und aktuellsten Daten zu sehen. Die beste Lösung hängt hauptsächlich wie die Datenquelle des Netzwerks die Daten ausgibt. In jedem Fall sollte remote Schlüssel ermöglichen das Speichern von Informationen zur letzten Seite. die vom Server angefordert werden. Anhand dieser Informationen kann die App fordern Sie die richtige Datenseite an, die als Nächstes geladen werden soll.

Fernbedienungstasten verwalten

Remote-Schlüssel sind Schlüssel, mit denen eine RemoteMediator-Implementierung dem Back-End-Dienst, welche Daten als Nächstes geladen werden sollen. Im einfachsten Fall kann jedes Element enthält eine Remote-Taste, auf die Sie ganz einfach verweisen können. Wenn die Remote-Tasten nicht einzelnen Elementen entsprechen, müssen Sie diese speichern. separat verwalten und in der load()-Methode verwalten.

In diesem Abschnitt wird beschrieben, wie Sie Remoteschlüssel erfassen, speichern und aktualisieren können, nicht in einzelnen Elementen gespeichert.

Artikelschlüssel

In diesem Abschnitt wird beschrieben, wie Sie mit Remote-Tasten arbeiten, die den entsprechenden einzelne Elemente. Wenn ein API einzelne Elemente ausschlüsselt, wird das Element normalerweise Die ID wird als Suchparameter übergeben. Der Parametername gibt an, ob der mit Artikeln vor oder nach der angegebenen ID antworten sollte. Im Beispiel der Modellklasse User wird das Feld id vom Server als Remote- wenn Sie zusätzliche Daten anfordern.

Wenn mit der Methode load() artikelspezifische Remote-Tasten verwaltet werden müssen, sind diese Tasten sind normalerweise die IDs der vom Server abgerufenen Daten. Aktualisierungsvorgänge benötigen keinen Ladeschlüssel, da sie nur die neuesten Daten abrufen. Ebenso müssen Vorgänge beim Voranstellen keine zusätzlichen Daten abgerufen werden, Bei Aktualisierung werden immer die neuesten Daten vom Server abgerufen.

Für Anfügevorgänge ist jedoch eine ID erforderlich. Dazu müssen Sie die letzte aus der Datenbank und laden Sie mit seiner ID die nächste Seite mit Daten. Wenn es keine Elemente in der Datenbank vorhanden sind, wird endOfPaginationReached auf "true" gesetzt, gibt an, dass die Daten aktualisiert werden müssen.

Kotlin

@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
  private val query: String,
  private val database: RoomDb,
  private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
  val userDao = database.userDao()

  override suspend fun load(
    loadType: LoadType,
    state: PagingState<Int, User>
  ): MediatorResult {
    return try {
      // The network load method takes an optional String
      // parameter. For every page after the first, pass the String
      // token returned from the previous page to let it continue
      // from where it left off. For REFRESH, pass null to load the
      // first page.
      val loadKey = when (loadType) {
        LoadType.REFRESH -> null
        // In this example, you never need to prepend, since REFRESH
        // will always load the first page in the list. Immediately
        // return, reporting end of pagination.
        LoadType.PREPEND -> return MediatorResult.Success(
          endOfPaginationReached = true
        )
        // Get the last User object id for the next RemoteKey.
        LoadType.APPEND -> {
          val lastItem = state.lastItemOrNull()

          // You must explicitly check if the last item is null when
          // appending, since passing null to networkService is only
          // valid for initial load. If lastItem is null it means no
          // items were loaded after the initial REFRESH and there are
          // no more items to load.
          if (lastItem == null) {
            return MediatorResult.Success(
              endOfPaginationReached = true
            )
          }

          lastItem.id
        }
      }

      // Suspending network load via Retrofit. This doesn't need to
      // be wrapped in a withContext(Dispatcher.IO) { ... } block
      // since Retrofit's Coroutine CallAdapter dispatches on a
      // worker thread.
      val response = networkService.searchUsers(query, loadKey)

      // Store loaded data, and next key in transaction, so that
      // they're always consistent.
      database.withTransaction {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query)
        }

        // Insert new users into database, which invalidates the
        // current PagingData, allowing Paging to present the updates
        // in the DB.
        userDao.insertAll(response.users)
      }

      // End of pagination has been reached if no users are returned from the
      // service
      MediatorResult.Success(
        endOfPaginationReached = response.users.isEmpty()
      )
    } catch (e: IOException) {
      MediatorResult.Error(e)
    } catch (e: HttpException) {
      MediatorResult.Error(e)
    }
  }
}

Java

@NotNull
@Override
public Single>MediatorResult< loadSingle(
  @NotNull LoadType loadType,
  @NotNull PagingState>Integer, User< state
) {
  // The network load method takes an optional String parameter. For every page
  // after the first, pass the String token returned from the previous page to
  // let it continue from where it left off. For REFRESH, pass null to load the
  // first page.
  Single>String< remoteKeySingle = null;
  switch (loadType) {
    case REFRESH:
      // Initial load should use null as the page key, so you can return null
      // directly.
      remoteKeySingle = Single.just(null);
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Single.just(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when
      // appending, since passing null to networkService is only
      // valid for initial load. If lastItem is null it means no
      // items were loaded after the initial REFRESH and there are
      // no more items to load.
      if (lastItem == null) {
        return Single.just(new MediatorResult.Success(true));
      }
      remoteKeySingle = Single.just(lastItem.getId());
      break;
  }

  return remoteKeySingle
    .subscribeOn(Schedulers.io())
    .flatMap((Function<String, Single<MediatorResult>>) remoteKey -> {
      return networkService.searchUsers(query, remoteKey)
        .map(response -> {
          database.runInTransaction(() -> {
            if (loadType == LoadType.REFRESH) {
              userDao.deleteByQuery(query);
            }
            // Insert new users into database, which invalidates the current
            // PagingData, allowing Paging to present the updates in the DB.
            userDao.insertAll(response.getUsers());
          });

          return new MediatorResult.Success(response.getUsers().isEmpty());
        });
    })
    .onErrorResumeNext(e -> {
      if (e instanceof IOException || e instanceof HttpException) {
        return Single.just(new MediatorResult.Error(e));
      }

      return Single.error(e);
    });
}

Java

@NotNull
@Override
public ListenableFuture<MediatorResult> loadFuture(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional after=<user.id> parameter.
  // For every page after the first, pass the last user ID to let it continue
  // from where it left off. For REFRESH, pass null to load the first page.
  ResolvableFuture<String> remoteKeyFuture = ResolvableFuture.create();
  switch (loadType) {
    case REFRESH:
      remoteKeyFuture.set(null);
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Futures.immediateFuture(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when appending,
      // since passing null to networkService is only valid for initial load.
      // If lastItem is null it means no items were loaded after the initial
      // REFRESH and there are no more items to load.
      if (lastItem == null) {
        return Futures.immediateFuture(new MediatorResult.Success(true));
      }

      remoteKeyFuture.set(lastItem.getId());
      break;
  }

  return Futures.transformAsync(remoteKeyFuture, remoteKey -> {

    ListenableFuture<MediatorResult> networkResult = Futures.transform(
      networkService.searchUsers(query, remoteKey),
      response -> {
        database.runInTransaction(() -> {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query);
        }

        // Insert new users into database, which invalidates the current
        // PagingData, allowing Paging to present the updates in the DB.
        userDao.insertAll(response.getUsers());
      });

      return new MediatorResult.Success(response.getUsers().isEmpty());
    }, bgExecutor);

    ListenableFuture<MediatorResult> ioCatchingNetworkResult =
      Futures.catching(
        networkResult,
        IOException.class,
        MediatorResult.Error::new,
        bgExecutor
      );

    return Futures.catching(
      ioCatchingNetworkResult,
      HttpException.class,
      MediatorResult.Error::new,
      bgExecutor
    );
  }, bgExecutor);
}

Seitentasten

In diesem Abschnitt wird beschrieben, wie Sie mit Remote-Tasten arbeiten, die nicht der einzelne Elemente.

Tabelle mit Remote-Schlüsseln hinzufügen

Wenn die Fernbedienungstasten nicht direkt mit Listenelementen verknüpft sind, in einer separaten Tabelle in der lokalen Datenbank speichern. Definieren Sie eine Raumentität, die stellt eine Tabelle mit Remote-Tasten dar:

Kotlin

@Entity(tableName = "remote_keys")
data class RemoteKey(val label: String, val nextKey: String?)

Java

@Entity(tableName = "remote_keys")
public class RemoteKey {
  public String label;
  public String nextKey;
}

Java

@Entity(tableName = "remote_keys")
public class RemoteKey {
  public String label;
  public String nextKey;
}

Sie müssen auch einen DAO für die Entität RemoteKey definieren:

Kotlin

@Dao
interface RemoteKeyDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insertOrReplace(remoteKey: RemoteKey)

  @Query("SELECT * FROM remote_keys WHERE label = :query")
  suspend fun remoteKeyByQuery(query: String): RemoteKey

  @Query("DELETE FROM remote_keys WHERE label = :query")
  suspend fun deleteByQuery(query: String)
}

Java

@Dao
interface RemoteKeyDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertOrReplace(RemoteKey remoteKey);

  @Query("SELECT * FROM remote_keys WHERE label = :query")
  Single<RemoteKey> remoteKeyByQuerySingle(String query);

  @Query("DELETE FROM remote_keys WHERE label = :query")
  void deleteByQuery(String query);
}

Java

@Dao
interface RemoteKeyDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  void insertOrReplace(RemoteKey remoteKey);

  @Query("SELECT * FROM remote_keys WHERE label = :query")
  ListenableFuture<RemoteKey> remoteKeyByQueryFuture(String query);

  @Query("DELETE FROM remote_keys WHERE label = :query")
  void deleteByQuery(String query);
}

Mit Remote-Tasten laden

Wenn mit der Methode load() Remote-Seitenschlüssel verwaltet werden müssen, müssen Sie sie definieren anders als bei der grundlegenden Nutzung RemoteMediator:

  • Fügen Sie eine zusätzliche Property hinzu, die einen Verweis auf den DAO für Ihr Tabelle mit Remote-Schlüsseln.
  • Bestimmen Sie, welcher Schlüssel als Nächstes geladen werden soll, indem Sie die Remote-Schlüsseltabelle abfragen mit PagingState.
  • Fügen Sie den zurückgegebenen Remoteschlüssel aus der Netzwerkdatenquelle ein oder speichern Sie ihn in zusätzlich zu den Seitendaten selbst.

Kotlin

@OptIn(ExperimentalPagingApi::class)
class ExampleRemoteMediator(
  private val query: String,
  private val database: RoomDb,
  private val networkService: ExampleBackendService
) : RemoteMediator<Int, User>() {
  val userDao = database.userDao()
  val remoteKeyDao = database.remoteKeyDao()

  override suspend fun load(
    loadType: LoadType,
    state: PagingState<Int, User>
  ): MediatorResult {
    return try {
      // The network load method takes an optional String
      // parameter. For every page after the first, pass the String
      // token returned from the previous page to let it continue
      // from where it left off. For REFRESH, pass null to load the
      // first page.
      val loadKey = when (loadType) {
        LoadType.REFRESH -> null
        // In this example, you never need to prepend, since REFRESH
        // will always load the first page in the list. Immediately
        // return, reporting end of pagination.
        LoadType.PREPEND -> return MediatorResult.Success(
          endOfPaginationReached = true
        )
        // Query remoteKeyDao for the next RemoteKey.
        LoadType.APPEND -> {
          val remoteKey = database.withTransaction {
            remoteKeyDao.remoteKeyByQuery(query)
          }

          // You must explicitly check if the page key is null when
          // appending, since null is only valid for initial load.
          // If you receive null for APPEND, that means you have
          // reached the end of pagination and there are no more
          // items to load.
          if (remoteKey.nextKey == null) {
            return MediatorResult.Success(
              endOfPaginationReached = true
            )
          }

          remoteKey.nextKey
        }
      }

      // Suspending network load via Retrofit. This doesn't need to
      // be wrapped in a withContext(Dispatcher.IO) { ... } block
      // since Retrofit's Coroutine CallAdapter dispatches on a
      // worker thread.
      val response = networkService.searchUsers(query, loadKey)

      // Store loaded data, and next key in transaction, so that
      // they're always consistent.
      database.withTransaction {
        if (loadType == LoadType.REFRESH) {
          remoteKeyDao.deleteByQuery(query)
          userDao.deleteByQuery(query)
        }

        // Update RemoteKey for this query.
        remoteKeyDao.insertOrReplace(
          RemoteKey(query, response.nextKey)
        )

        // Insert new users into database, which invalidates the
        // current PagingData, allowing Paging to present the updates
        // in the DB.
        userDao.insertAll(response.users)
      }

      MediatorResult.Success(
        endOfPaginationReached = response.nextKey == null
      )
    } catch (e: IOException) {
      MediatorResult.Error(e)
    } catch (e: HttpException) {
      MediatorResult.Error(e)
    }
  }
}

Java

@NotNull
@Override
public Single<MediatorResult> loadSingle(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional String parameter. For every page
  // after the first, pass the String token returned from the previous page to
  // let it continue from where it left off. For REFRESH, pass null to load the
  // first page.
  Single<RemoteKey> remoteKeySingle = null;
  switch (loadType) {
    case REFRESH:
      // Initial load should use null as the page key, so you can return null
      // directly.
      remoteKeySingle = Single.just(new RemoteKey(mQuery, null));
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Single.just(new MediatorResult.Success(true));
    case APPEND:
      // Query remoteKeyDao for the next RemoteKey.
      remoteKeySingle = mRemoteKeyDao.remoteKeyByQuerySingle(mQuery);
      break;
  }

  return remoteKeySingle
    .subscribeOn(Schedulers.io())
    .flatMap((Function<RemoteKey, Single<MediatorResult>>) remoteKey -> {
      // You must explicitly check if the page key is null when appending,
      // since null is only valid for initial load. If you receive null
      // for APPEND, that means you have reached the end of pagination and
      // there are no more items to load.
      if (loadType != REFRESH && remoteKey.getNextKey() == null) {
        return Single.just(new MediatorResult.Success(true));
      }

      return networkService.searchUsers(query, remoteKey.getNextKey())
        .map(response -> {
          database.runInTransaction(() -> {
            if (loadType == LoadType.REFRESH) {
              userDao.deleteByQuery(query);
              remoteKeyDao.deleteByQuery(query);
            }

            // Update RemoteKey for this query.
            remoteKeyDao.insertOrReplace(new RemoteKey(query, response.getNextKey()));

            // Insert new users into database, which invalidates the current
            // PagingData, allowing Paging to present the updates in the DB.
            userDao.insertAll(response.getUsers());
          });

          return new MediatorResult.Success(response.getNextKey() == null);
        });
    })
    .onErrorResumeNext(e -> {
      if (e instanceof IOException || e instanceof HttpException) {
        return Single.just(new MediatorResult.Error(e));
      }

      return Single.error(e);
    });
}

Java

@NotNull
@Override
public ListenableFuture<MediatorResult> loadFuture(
  @NotNull LoadType loadType,
  @NotNull PagingState<Integer, User> state
) {
  // The network load method takes an optional after=<user.id> parameter. For
  // every page after the first, pass the last user ID to let it continue from
  // where it left off. For REFRESH, pass null to load the first page.
  ResolvableFuture<RemoteKey> remoteKeyFuture = ResolvableFuture.create();
  switch (loadType) {
    case REFRESH:
      remoteKeyFuture.set(new RemoteKey(query, null));
      break;
    case PREPEND:
      // In this example, you never need to prepend, since REFRESH will always
      // load the first page in the list. Immediately return, reporting end of
      // pagination.
      return Futures.immediateFuture(new MediatorResult.Success(true));
    case APPEND:
      User lastItem = state.lastItemOrNull();

      // You must explicitly check if the last item is null when appending,
      // since passing null to networkService is only valid for initial load.
      // If lastItem is null it means no items were loaded after the initial
      // REFRESH and there are no more items to load.
      if (lastItem == null) {
        return Futures.immediateFuture(new MediatorResult.Success(true));
      }

      // Query remoteKeyDao for the next RemoteKey.
      remoteKeyFuture.setFuture(
        remoteKeyDao.remoteKeyByQueryFuture(query));
      break;
  }

  return Futures.transformAsync(remoteKeyFuture, remoteKey -> {
    // You must explicitly check if the page key is null when appending,
    // since null is only valid for initial load. If you receive null
    // for APPEND, that means you have reached the end of pagination and
    // there are no more items to load.
    if (loadType != LoadType.REFRESH && remoteKey.getNextKey() == null) {
      return Futures.immediateFuture(new MediatorResult.Success(true));
    }

    ListenableFuture<MediatorResult> networkResult = Futures.transform(
      networkService.searchUsers(query, remoteKey.getNextKey()),
      response -> {
        database.runInTransaction(() -> {
        if (loadType == LoadType.REFRESH) {
          userDao.deleteByQuery(query);
          remoteKeyDao.deleteByQuery(query);
        }

        // Update RemoteKey for this query.
        remoteKeyDao.insertOrReplace(new RemoteKey(query, response.getNextKey()));

        // Insert new users into database, which invalidates the current
        // PagingData, allowing Paging to present the updates in the DB.
        userDao.insertAll(response.getUsers());
      });

      return new MediatorResult.Success(response.getNextKey() == null);
    }, bgExecutor);

    ListenableFuture<MediatorResult> ioCatchingNetworkResult =
      Futures.catching(
        networkResult,
        IOException.class,
        MediatorResult.Error::new,
        bgExecutor
      );

    return Futures.catching(
      ioCatchingNetworkResult,
      HttpException.class,
      MediatorResult.Error::new,
      bgExecutor
    );
  }, bgExecutor);
}

Vor Ort aktualisieren

Wenn Ihre App nur Netzwerkaktualisierungen von oben in der Liste als vorherigen Beispielen gezeigt haben, muss Ihr RemoteMediator nicht definieren, Ladeverhalten voranstellen.

Wenn Ihre App jedoch das inkrementelle Laden aus dem Netzwerk unterstützen muss, in die lokale Datenbank importieren, müssen Sie die Paginierung beginnend am Anker, der Scrollposition der Nutzenden. PagingSource des Raums die Implementierung übernimmt. Wenn Sie den Raum nicht nutzen, indem Sie die PagingSource.getRefreshKey() Ein Beispiel für die Implementierung von getRefreshKey() finden Sie unter Definieren des PagingSource.

In Abbildung 4 sehen Sie, wie Daten zuerst aus der lokalen Datenbank geladen werden. und dann aus dem Netzwerk, sobald die Daten der Datenbank aufgebraucht sind.

<ph type="x-smartling-placeholder">
</ph> PagingSource wird aus der Datenbank in die Benutzeroberfläche geladen, bis die Datenbank
    keine Daten mehr hat. Dann wird der RemoteMediator aus dem Netzwerk in den
    und anschließend wird PagingSource weiter geladen.
<ph type="x-smartling-placeholder">
</ph> Abbildung 4: Diagramm, das die Funktionsweise von PagingSource und RemoteMediator zeigt um Daten zu laden.

Weitere Informationen

Weitere Informationen zur Paging-Bibliothek finden Sie in den folgenden zusätzlichen Ressourcen:

Codelabs

Produktproben