Strona z sieci i bazy danych

Zadbaj o to, aby użytkownicy mogli korzystać z Twojej aplikacji, aby mogli z niej korzystać. gdy połączenie sieciowe jest zawodne lub użytkownik jest offline. W jedną stronę strony z sieci i lokalnej bazy danych w tym samym czasie. Dzięki temu aplikacja będzie obsługiwać interfejs z lokalnej pamięci podręcznej bazy danych i będzie miał do sieci, gdy w bazie danych nie ma więcej danych.

W tym przewodniku zakładamy, że wiesz już, czym jest trwałość sal i podstawowe korzystanie z funkcji Paging Biblioteka.

Wczytywanie danych współrzędnych

Biblioteka stronnicza udostępnia funkcje Komponent RemoteMediator dla tego przypadku użycia. RemoteMediator działa jako sygnał z biblioteki stronicowania gdy aplikacja wyczerpuje dane w pamięci podręcznej. Możesz użyć tego sygnału do wczytywania dodatkowych danych z sieci i zapisują je w lokalnej bazie danych, gdzie Aplikacja PagingSource może ją załadować, i przekazywać je do interfejsu.

Gdy potrzebne są dodatkowe dane, biblioteka stron internetowych wywołuje metodę load() z w implementacji RemoteMediator. Jest to funkcja zawieszania, która jest bezpieczna mogą pracować długofalowo. Ta funkcja zwykle pobiera nowe dane z do źródła sieciowego i zapisze je w pamięci lokalnej.

Ten proces działa z nowymi danymi, ale z czasem danymi przechowywanymi w bazie danych wymaga unieważnienia, na przykład wtedy, gdy użytkownik ręcznie aktywuje odświeżenie. Ten jest reprezentowany przez LoadType. do metody load(). LoadType informuje RemoteMediator o konieczności odświeżenia istniejących danych czy pobrania danych. dane, które należy dołączyć do istniejącej listy.

Dzięki temu RemoteMediator upewnia się, że aplikacja wczytuje dane, które które użytkownicy chcą oglądać w odpowiedniej kolejności.

Cykl życia stron

. Rysunek 1. Schemat cyklu życia stronicowania z użyciem PagingSource i Dane stronicowania.

W przypadku stronicowania bezpośrednio z sieci PagingSource wczytuje dane i zwraca LoadResult. obiektu. Implementacja PagingSource jest przekazywana do Pager przez pagingSourceFactory.

Ponieważ interfejs użytkownika wymaga nowych danych, funkcja Pager wywołuje metodę load() z metody PagingSource i zwraca strumień o wartości PagingData obiekty, które i zastosować nowe dane. Każdy obiekt PagingData jest zwykle przechowywany w pamięci podręcznej ViewModel.

. Rysunek 2. Schemat cyklu życia stronicowania z użyciem PagingSource i RemoteMediator.

RemoteMediator zmienia ten przepływ danych. PagingSource nadal wczytuje dane. ale po wyczerpaniu danych stronicowanych biblioteka stron uruchamia RemoteMediator, aby wczytać nowe dane ze źródła sieciowego. RemoteMediator przechowuje nowe dane w lokalnej bazie danych, więc pamięć podręczna w pamięci jest Pole ViewModel jest niepotrzebne. I wreszcie, PagingSource unieważnia się sam, Pager tworzy nową instancję do wczytania aktualnych danych z bazy danych.

Podstawowe wykorzystanie

Załóżmy, że chcesz, by Twoja aplikacja wczytywała strony z User elementami z poziomu elementu z kluczem do lokalnej pamięci podręcznej przechowywanej w bazie danych sal.

RemoteMediator wczytuje dane z sieci do bazy danych oraz
    Strona PagingSource wczytuje dane z bazy danych. Pager wykorzystuje zarówno
    RemoteMediator i PagingSource do wczytywania danych ze stron.
. Rysunek 3. Schemat implementacji stronicowej, która korzysta z danych warstwowych źródła.

Implementacja RemoteMediator pomaga wczytywać dane stron z sieci do bazy danych, ale nie wczytuje danych bezpośrednio do interfejsu użytkownika. Zamiast tego aplikacja używa jako źródło danych truth. Inaczej mówiąc, tylko aplikacja wyświetla dane przechowywane w pamięci podręcznej bazy danych. PagingSource implementacja (np. wygenerowana przez Salę) obsługuje wczytywanie danych z pamięci podręcznej z bazy danych do interfejsu użytkownika.

Tworzenie elementów pokoju

Pierwszym krokiem jest użycie opcji Trwałość sal do zdefiniowania bazy danych, która zawiera lokalnej pamięci podręcznej danych stronicowanych z sieciowego źródła danych. Zacznij od wdrożenie RoomDatabase zgodnie z opisem w sekcji Zapisywanie danych w lokalnej bazie danych za pomocą Sala.

Następnie zdefiniuj element „Pokój”, aby reprezentować tabelę elementów listy zgodnie z opisem w sekcji Definiowanie danych za pomocą elementów pokoju Nadaj mu pole id jako klucz podstawowy oraz pola dla innych pól informacje zawarte w elementach listy.

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;
}

Musisz też zdefiniować obiekt dostępu do danych (DAO) dla tej encji w pokoju jako opisane w sekcji Uzyskiwanie dostępu do danych za pomocą Pokoju DAO. DAO dla elementu listy element musi zawierać następujące metody:

  • Metoda insertAll(), która wstawia listę elementów do tabeli.
  • Metoda, która przyjmuje ciąg zapytania jako parametr i zwraca PagingSource na listę wyników. Dzięki temu obiekt Pager może użyj tej tabeli jako źródła danych stron.
  • Metoda clearAll(), która usuwa wszystkie dane tabeli.

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();
}

Implementowanie obiektu RemoteMediator

Główną rolą RemoteMediator jest wczytywanie większej ilości danych z sieci, albo brakuje danych w narzędziu Pager, albo dane są unieważnione. it zawiera metodę load(), którą musisz zastąpić, aby zdefiniować wczytywanie zachowanie użytkownika.

Typowa implementacja RemoteMediator obejmuje te parametry:

  • query: ciąg zapytania określający dane, które mają zostać pobrane z backendu. posprzedażna.
  • database: baza danych sal, która służy jako lokalna pamięć podręczna.
  • networkService: instancja interfejsu API usługi backendu.

Utwórz implementację RemoteMediator<Key, Value>. typy Key oraz Typ Value powinien być taki sam, jak w przypadku definiowania atrybutu PagingSource w odniesieniu do tego samego sieciowego źródła danych. Więcej informacji na temat: w sekcji dotyczącej wybierania parametrów typów można znaleźć informacje na temat wybierania klucza i wartości. .

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
  ) {
    ...
  }
}

Metoda load() odpowiada za aktualizowanie bazowego zbioru danych oraz unieważniając PagingSource. Niektóre biblioteki obsługujące stronicowanie (np. Pokoje) będzie automatycznie obsługiwać unieważnianie PagingSource obiektów, które zostały do wdrożenia.

Metoda load() przyjmuje 2 parametry:

  • PagingState, który zawiera informacje o wczytanych do tej pory stronach, ostatnio otwieranym indeksie oraz PagingConfig który został użyty do zainicjowania strumienia stronicowania.
  • LoadType, która wskazuje wartość typ obciążenia: REFRESH, APPEND lub PREPEND.

Wartość zwrócona przez metodę load() to MediatorResult. obiektu. MediatorResult może być MediatorResult.Error. (w tym opis błędu) lub MediatorResult.Success (który zawiera sygnał informujący o tym, czy można wczytać więcej danych).

Metoda load() musi wykonać te czynności:

  1. Ustal, którą stronę załadować z sieci, w zależności od typu obciążenia i które zostały już wczytane do tej pory.
  2. Wywołaj żądanie sieciowe.
  3. Wykonaj działania w zależności od wyniku operacji wczytywania:
    • Jeśli wczytywanie zostało ukończone, a otrzymana lista elementów nie jest pusta, zapisz elementy listy w bazie danych i zwracaj MediatorResult.Success(endOfPaginationReached = false) Po zgromadzeniu danych , unieważni źródło danych w celu powiadomienia biblioteki stronniczej nowych danych.
    • Jeśli wczytywanie się udało, a odebrana lista elementów jest pusta lub jest to ostatni indeks strony, a następnie zwraca MediatorResult.Success(endOfPaginationReached = true) Po odtworzeniu danych unieważni źródło danych, aby powiadomić bibliotekę stronniczą o nowym i skalowalnych danych.
    • Jeśli żądanie powoduje błąd, zwraca wartość MediatorResult.Error.

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
  );
}

Zdefiniuj metodę inicjowania

Implementacje RemoteMediator mogą też zastąpić parametr initialize(). , aby sprawdzić, czy dane w pamięci podręcznej są nieaktualne i zdecydować, czy mają być zdalne odświeżanie. Metoda ta jest uruchamiana przed rozpoczęciem wczytywania, dzięki czemu można manipulowanie bazą danych (np. w celu wyczyszczenia starych danych) przed uruchomieniem dowolnej załadunku lokalnego lub zdalnego.

initialize() jest funkcją asynchroniczną, więc dane można pobierać w określania trafności danych znajdujących się w bazie danych. Najczęstsze jest tak, że dane przechowywane w pamięci podręcznej są ważne tylko przez określony czas. RemoteMediator może sprawdzić, czy ten czas wygaśnięcia upłynął. W którym , jeśli biblioteka stron internetowych musi całkowicie odświeżyć dane. Implementacje Funkcja initialize() powinna zwracać taki błąd: InitializeAction

  • Jeśli dane lokalne wymagają pełnego odświeżenia, initialize() powinien zwrócić InitializeAction.LAUNCH_INITIAL_REFRESH Powoduje to, że RemoteMediator wykonuje zdalne odświeżanie, aby całkowicie ponownie załadować stronę. danych. Wszystkie zdalne ładunki APPEND lub PREPEND czekają na wczytanie REFRESH aby odnieść sukces, zanim przejdziesz dalej.
  • Jeśli dane lokalne nie muszą być odświeżane, initialize() powinien zwrócić InitializeAction.SKIP_INITIAL_REFRESH Dzięki temu RemoteMediator pomija zdalne odświeżanie i wczytuje plik z pamięci podręcznej.

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);
}

Utwórz pager

Na koniec musisz utworzyć instancję Pager, aby skonfigurować strumień danych stronicowanych. Przypomina to tworzenie obiektu Pager na podstawie prostego sieciowego źródła danych, ale obowiązują 2 rzeczy, które należy zrobić inaczej:

  • Zamiast bezpośrednio przekazywać konstruktor PagingSource, musisz podać która zwraca obiekt PagingSource z DAO.
  • Musisz podać instancję swojej implementacji RemoteMediator jako remoteMediator.

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));

Obsługa warunków wyścigu

Jedna z sytuacji, którą musi obsługiwać aplikacja przy wczytywaniu danych z wielu źródeł to sytuacja, w której dane z lokalnej pamięci podręcznej nie są zsynchronizowane z zdalnego źródła danych.

Kiedy zwracana jest metoda initialize() z implementacji RemoteMediator LAUNCH_INITIAL_REFRESH, dane są nieaktualne i trzeba je zastąpić nowymi i skalowalnych danych. W przypadku żądań obciążenia PREPEND lub APPEND wymuszane jest oczekiwanie na pilota Liczba załadowań: REFRESH. Ponieważ prośby PREPEND lub APPEND zostały znajduje się w kolejce przed żądaniem REFRESH, możliwe, że PagingState przekazywane do tych wywołań wczytywania staną się nieaktualne w momencie ich uruchomienia.

W zależności od tego, jak dane są przechowywane lokalnie, aplikacja może zignorować nadmiarowe dane. wysyła żądania, jeśli zmiany danych w pamięci podręcznej spowodują unieważnienie i pobranie nowych danych. Na przykład Sala unieważnia zapytania dotyczące wstawiania danych. Oznacza to, że nowe Do oczekującego wczytania przekazano PagingSource obiektu z odświeżonymi danymi żądań po wstawieniu nowych danych do bazy danych.

Rozwiązanie tego problemu z synchronizacją danych ma kluczowe znaczenie, możesz wyświetlać najbardziej trafne i aktualne dane. Najlepsze rozwiązanie zależy głównie od sposób, w jaki źródło danych sieci dodaje dane do stron. W każdym przypadku pilot pozwala zapisać informacje o ostatniej stronie żądania z serwera. Aplikacja może używać tych informacji do identyfikowania zażądać następnej strony z danymi.

Zarządzaj kluczykami zdalnymi

Klucze zdalne to klucze, których implementacja RemoteMediator używa do przekazywania funkcji w usłudze backendu, które dane mają zostać wczytane w następnej kolejności. W najprostszym przypadku każdy element dane z podziałem na strony zawierają klucz zdalny, do którego możesz się łatwo odwołać. Jeśli jednak kluczy zdalnych nie odpowiada poszczególnym produktom, musisz je zapisać oddzielnie i zarządzać nimi w metodzie load().

W tej sekcji dowiesz się, jak zebrać, przechowywać i aktualizować klucze zdalne, które są nie są przechowywane w poszczególnych elementach.

Klucze elementów

W tej sekcji opisano sposób pracy z klawiszami zdalnymi, które odpowiadają poszczególnych elementów. Zwykle, gdy klucz interfejsu API przenosi się do poszczególnych produktów, Identyfikator jest przekazywany jako parametr zapytania. Nazwa parametru wskazuje, czy serwer powinien odpowiadać, przesyłając produkty przed podanym identyfikatorem lub po nim. W tym przykładzie klasy modelu User, pole id z serwera jest używane jako zdalne przy żądaniu dodatkowych danych.

Gdy metoda load() musi zarządzać kluczami zdalnymi związanymi z danym produktem, te klucze zwykle są identyfikatory danych pobranych z serwera. Operacje odświeżania nie potrzebują klucza wczytywania, bo pobierają tylko najnowsze dane. Operacje dołączania na początku nie wymagają też pobierania żadnych dodatkowych danych, ponieważ odświeżanie zawsze pobiera najnowsze dane z serwera.

Operacje dołączania wymagają jednak identyfikatora. Wymaga to wczytania ostatniego z bazy danych i użyj identyfikatora do wczytania następnej strony danych. Jeśli nie ma elementów w bazie danych, to endOfPaginationReached ma wartość Prawda, co sygnalizuje konieczność odświeżenia danych.

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);
}

Klucze strony

Ta sekcja opisuje sposób pracy z klawiszami zdalnymi, które nie odpowiadają poszczególnych elementów.

Dodaj tabelę kluczy zdalnych

Gdy klawisze zdalne nie są bezpośrednio powiązane z elementami listy, najlepiej jest i przechowywać je w osobnej tabeli w lokalnej bazie danych. Zdefiniuj element pokoju, który reprezentuje tabelę kluczy zdalnych:

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;
}

Musisz też zdefiniować DAO dla elementu RemoteKey:

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);
}

Ładowanie przy użyciu klawiszy zdalnych

Jeśli metoda load() musi zarządzać zdalnymi kluczami stron, musisz ją zdefiniować inaczej niż podstawowe RemoteMediator:

  • Dodaj dodatkową usługę zawierającą odniesienie do DAO dla Twojej tabeli kluczy zdalnych.
  • Określ klucz do wczytania w następnej kolejności, wysyłając zapytanie do tabeli kluczy zdalnych, a nie za pomocą funkcji PagingState.
  • Wstaw lub zapisz zwrócony klucz zdalny z sieciowego źródła danych w oprócz danych stron.

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);
}

Odświeżanie

Jeśli aplikacja musi obsługiwać tylko odświeżanie sieci od góry listy jako w poprzednich przykładach, RemoteMediator nie musi określać zachowanie wczytywania na początku.

Jeśli jednak aplikacja musi obsługiwać przyrostowe wczytywanie z sieci do lokalnej bazy danych, musisz zapewnić obsługę wznowienia podziału na strony. od reklamy zakotwiczonej, czyli pozycji przewijania użytkownika. PagingSource w pokoju zajmuje się tym za Ciebie, ale jeśli nie korzystasz z Room, to przez zastąpienie PagingSource.getRefreshKey() Przykład implementacji getRefreshKey() znajdziesz w sekcji Definiowanie Strona PagingSource

Rysunek 4 przedstawia proces wczytywania danych z lokalnej bazy danych, a potem z sieci, gdy w bazie danych brakuje danych.

Strona PagingSource jest ładowana z bazy danych do interfejsu użytkownika do momentu, aż zostanie ona wczytana
    wyczerpał się pakiet danych. Następnie RemoteMediator wczytuje dane z sieci do pliku
    , a potem strona PagingSource będzie wczytywała się dalej.
. Rysunek 4. Diagram przedstawiający sposób działania stron PagingSource i RemoteMediator jednocześnie, by wczytać dane.

Dodatkowe materiały

Więcej informacji o bibliotece stronicowania znajdziesz w tych dodatkowych materiałach:

Ćwiczenia z programowania

Próbki

. .