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
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
.
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.
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 obiektPager
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 orazPagingConfig
który został użyty do zainicjowania strumienia stronicowania.LoadType
, która wskazuje wartość typ obciążenia:REFRESH
,APPEND
lubPREPEND
.
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:
- 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.
- Wywołaj żądanie sieciowe.
- 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ę stronicowania o nowym i skalowalnych danych. - Jeśli żądanie powoduje błąd, zwraca wartość
MediatorResult.Error
.
- Jeśli wczytywanie zostało ukończone, a otrzymana lista elementów nie jest pusta,
zapisz elementy listy w bazie danych i zwracaj
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, żeRemoteMediator
wykonuje zdalne odświeżanie, aby całkowicie ponownie załadować stronę. danych. Wszystkie zdalne ładunkiAPPEND
lubPREPEND
czekają na wczytanieREFRESH
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 temuRemoteMediator
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 obiektPagingSource
z DAO. - Musisz podać instancję swojej implementacji
RemoteMediator
jakoremoteMediator
.
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 żadnych 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.
Dodatkowe materiały
Więcej informacji o bibliotece stronicowania znajdziesz w tych dodatkowych materiałach:
Ćwiczenia z programowania
Próbki
.Polecane dla Ciebie
- Uwaga: tekst linku wyświetla się, gdy JavaScript jest wyłączony
- Wczytywanie i wyświetlanie danych z podziałem na strony
- Testowanie implementacji stronicowania
- Migracja do Paging 3