Zadbaj o wygodę użytkowników, upewniając się, że można jej używać, gdy połączenia sieciowe są zawodne lub gdy użytkownik jest offline. Można to robić na przykład z sieci i z lokalnej bazy danych. Dzięki temu aplikacja kieruje interfejs użytkownika z lokalnej pamięci podręcznej bazy danych i wysyła żądania do sieci tylko wtedy, gdy w bazie danych nie ma już więcej danych.
W tym przewodniku zakładamy, że znasz bibliotekę trwałości sal i znasz podstawowe korzystanie z biblioteki stronicowania.
Koordynuj wczytywanie danych
Biblioteka stronicowania udostępnia komponent RemoteMediator
na potrzeby tego przypadku użycia. RemoteMediator
działa jako sygnał z biblioteki stronicowania, gdy w aplikacji wyczerpią się dane w pamięci podręcznej. Możesz użyć tego sygnału, aby wczytać dodatkowe dane z sieci i zapisać je w lokalnej bazie danych, gdzie PagingSource
może je wczytać i przekazać do interfejsu w celu ich wyświetlenia.
Gdy potrzebne są dodatkowe dane, biblioteka stronicowania wywołuje metodę load()
z implementacji RemoteMediator
. To funkcja zawieszania, która pozwala
bezpiecznie wykonywać długotrwałe zadania. Ta funkcja zwykle pobiera nowe dane ze źródła sieci i zapisuje je w pamięci lokalnej.
Ten proces sprawdza się w przypadku nowych danych, ale z czasem przechowywane w bazie danych wymagają unieważnienia, na przykład gdy użytkownik ręcznie aktywuje odświeżenie. Reprezentuje ją właściwość LoadType
przekazywana do metody load()
. Parametr LoadType
informuje RemoteMediator
, czy musi odświeżyć istniejące dane czy pobrać dodatkowe dane, które trzeba dołączyć do dotychczasowej listy.
Dzięki temu RemoteMediator
będzie mieć pewność, że aplikacja wczytuje dane, które chcą zobaczyć użytkownicy, w odpowiedniej kolejności.
Cykl życia stron
W przypadku stronicowania bezpośrednio z sieci PagingSource
wczytuje dane i zwraca obiekt LoadResult
. Implementacja PagingSource
jest przekazywana do Pager
za pomocą parametru pagingSourceFactory
.
Interfejs użytkownika wymaga nowych danych, więc Pager
wywołuje metodę load()
z PagingSource
i zwraca strumień obiektów PagingData
, które zawierają nowe dane. Każdy obiekt PagingData
jest zwykle zapisywany w pamięci podręcznej w ViewModel
, zanim zostanie wysłany do interfejsu w celu wyświetlenia.
RemoteMediator
zmienia ten przepływ danych. Element PagingSource
nadal wczytuje dane, ale po wyczerpaniu danych stronicowanych biblioteka stronicowania aktywuje polecenie 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 podręcznej nie jest potrzebna w ViewModel
. Na koniec PagingSource
unieważnia się, a Pager
tworzy nową instancję, aby wczytać nowe dane z bazy danych.
Podstawowe użycie
Załóżmy, że chcesz, aby Twoja aplikacja wczytywała strony elementów User
ze źródła danych sieciowych z elementem jako kluczem do lokalnej pamięci podręcznej przechowywanej w bazie danych Room.
Implementacja RemoteMediator
ułatwia wczytywanie z sieci danych z podziałem na strony do bazy danych, ale nie wczytuje ich bezpośrednio do interfejsu. Zamiast tego aplikacja korzysta z bazy danych jako źródła danych. Innymi słowy, aplikacja wyświetla tylko dane, które zostały zapisane w pamięci podręcznej. Implementacja PagingSource
(np. wygenerowana przez Room) obsługuje wczytywanie do interfejsu danych z pamięci podręcznej z bazy danych.
Tworzenie elementów pokoju
Pierwszym krokiem jest zdefiniowanie bazy danych z lokalną pamięcią podręczną stronicowanych na podstawie biblioteki trwałości sal z sieciowego źródła danych. Zacznij od wdrożenia RoomDatabase
zgodnie z opisem w sekcji Zapisywanie danych w lokalnej bazie danych przy użyciu pokoju.
Następnie zdefiniuj element Room, który będzie reprezentował tabelę z elementami listy zgodnie z opisem w sekcji Definiowanie danych za pomocą elementów dotyczących sal.
Nadaj mu pole id
jako klucz podstawowy oraz pola na dowolne inne 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 dla tej encji sala zgodnie z opisem w sekcji Uzyskiwanie dostępu do danych za pomocą DAO dla sal. DAO elementu listy musi obejmować te metody:
- Metoda
insertAll()
, która wstawia listę elementów do tabeli. - Metoda, która pobiera ciąg zapytania jako parametr i zwraca obiekt
PagingSource
na liście wyników. Dzięki temu obiektPager
może użyć tej tabeli jako źródła danych stronicowanych. - Metoda
clearAll()
, która usuwa wszystkie dane z 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(); }
Implementacja zdalnego mediatora
Główną rolą funkcji RemoteMediator
jest wczytywanie z sieci większej ilości danych, gdy w interfejsie Pager
zabraknie danych lub dotychczasowe dane zostaną unieważnione. Zawiera metodę load()
, którą musisz zastąpić, by zdefiniować sposób ładowania.
Typowa implementacja RemoteMediator
obejmuje te parametry:
query
: ciąg zapytania określający dane do pobrania z usługi backendu.database
: baza danych sal, która pełni rolę lokalnej pamięci podręcznej.networkService
: instancja interfejsu API dla usługi backendu.
Utwórz implementację RemoteMediator<Key, Value>
. Typ Key
i typ Value
powinny być takie same jak w przypadku zdefiniowania obiektu PagingSource
dla tego samego źródła danych sieci. Więcej informacji o wybieraniu parametrów typów znajdziesz w artykule Wybieranie typu 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 zbioru danych kopii zapasowej i unieważnianie PagingSource
. Niektóre biblioteki obsługujące stronicowanie (np. Room) automatycznie obsługują unieważnione implementowane obiekty PagingSource
.
Metoda load()
przyjmuje 2 parametry:
PagingState
, który zawiera informacje o dotychczasowych wczytanych stronach, ostatnio otwieranym indeksie i obiekciePagingConfig
użytym do zainicjowania strumienia stronicowania.LoadType
, czyli typ wczytania:REFRESH
,APPEND
lubPREPEND
.
Wartość zwracana metody load()
jest obiektem MediatorResult
. MediatorResult
może mieć wartość MediatorResult.Error
(z opisem błędu) lub MediatorResult.Success
(zawierający sygnał informujący, czy jest więcej danych do wczytania).
Metoda load()
musi wykonywać te czynności:
- Określ, którą stronę wczytać z sieci, w zależności od typu obciążenia i dotychczasowych danych.
- Wywołaj żądanie sieciowe.
- Wykonaj działania w zależności od wyniku operacji wczytywania:
- Jeśli wczytanie się uda, a odebrana lista elementów nie jest pusta, zapisz elementy listy w bazie danych i zwróć wartość
MediatorResult.Success(endOfPaginationReached = false)
. Po zapisaniu danych unieważnij źródło danych, aby powiadomić bibliotekę stron o nowych danych. - Jeśli wczytywanie się powiodło, a otrzymana lista elementów jest pusta lub jest to ostatni indeks strony, zwróć wartość
MediatorResult.Success(endOfPaginationReached = true)
. Po zapisaniu danych unieważnij źródło danych, aby powiadomić bibliotekę stron o nowych danych. - Jeśli żądanie powoduje błąd, zwróć
MediatorResult.Error
.
- Jeśli wczytanie się uda, a odebrana lista elementów nie jest pusta, zapisz elementy listy w bazie danych i zwróć wartość
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ć metodę initialize()
, aby sprawdzić, czy dane w pamięci podręcznej są nieaktualne, i zdecydować, czy uruchomić odświeżanie zdalne. Ta metoda jest uruchamiana przed wczytaniem, dzięki czemu możesz wykonywać operacje na bazie danych (np. w celu usunięcia starych danych) przed uruchomieniem jakichkolwiek obciążeń lokalnych lub zdalnych.
initialize()
to funkcja asynchroniczna, możesz więc wczytywać dane, aby określić trafność tych informacji znajdujących się w bazie danych. Najczęstszym przypadkiem jest to, że dane z pamięci podręcznej są ważne tylko przez określony czas. RemoteMediator
może sprawdzić, czy upłynął czas wygaśnięcia. W takim przypadku biblioteka stronicowania musi w pełni odświeżyć dane. Implementacje funkcji initialize()
powinny zwracać błąd InitializeAction
w taki sposób:
- W przypadkach, gdy dane lokalne wymagają pełnego odświeżenia, funkcja
initialize()
powinna zwrócić wartośćInitializeAction.LAUNCH_INITIAL_REFRESH
. Powoduje to zdalne odświeżanieRemoteMediator
w celu pełnego ponownego załadowania danych. Przed kontynuacją wszystkie operacje zdalnego wczytywaniaAPPEND
lubPREPEND
muszą poczekać na pomyślne wczytanie elementuREFRESH
. - Jeśli dane lokalne nie wymagają odświeżania,
initialize()
powinien zwrócić wartośćInitializeAction.SKIP_INITIAL_REFRESH
. Powoduje to, żeRemoteMediator
pomija zdalne odświeżanie i wczytuje dane 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
z prostego źródła danych sieciowych, ale musisz zrobić 2 rzeczy w inny sposób:
- Zamiast przekazywać bezpośrednio konstruktor
PagingSource
, musisz podać metodę zapytania, która zwraca obiektPagingSource
z DAO. - Musisz podać wystąpienie implementacji
RemoteMediator
jako parametrremoteMediator
.
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));
Warunki wyścigu
Jedną z sytuacji, z którą aplikacja musi sobie poradzić podczas wczytywania danych z wielu źródeł, jest sytuacja, w której lokalne dane z pamięci podręcznej nie są zsynchronizowane ze zdalnym źródłem danych.
Gdy metoda initialize()
z Twojej implementacji RemoteMediator
zwraca LAUNCH_INITIAL_REFRESH
, dane są nieaktualne i trzeba je zastąpić nowymi danymi. Wszystkie żądania wczytywania PREPEND
lub APPEND
muszą czekać na prawidłowe wczytanie REFRESH
. Żądania PREPEND
lub APPEND
zostały umieszczone w kolejce przed żądaniem REFRESH
, dlatego może się zdarzyć, że żądanie PagingState
przekazane do tych wywołań wczytywania będzie nieaktualne w momencie ich uruchomienia.
W zależności od sposobu przechowywania danych lokalnie aplikacja może zignorować zbędne żądania, jeśli zmiany danych w pamięci podręcznej spowodują ich unieważnienie i ponowne pobranie.
Na przykład sala unieważnia zapytania dotyczące wstawiania danych. Oznacza to, że nowe obiekty PagingSource
z odświeżonymi danymi są dostarczane do oczekujących żądań obciążenia po wstawieniu do bazy danych nowych danych.
Rozwiązanie tego problemu z synchronizacją danych jest kluczowe, aby użytkownicy widzieli najbardziej odpowiednie i aktualne dane. Najlepsze rozwiązanie zależy głównie od sposobu, w jaki źródło danych udostępnia dane. W każdym przypadku klucze zdalne umożliwiają zapisywanie informacji o ostatniej stronie żądanej przez serwer. Dzięki tym informacjom można zidentyfikować właściwą stronę danych do wczytania i poprosić o nią.
Zarządzaj kluczykami zdalnymi
Klucze zdalne to klucze, których implementacja RemoteMediator
używa do informowania usługi backendu, które dane mają zostać wczytane w następnej kolejności. W najprostszym przypadku każdy element danych na stronie zawiera klucz zdalny, do którego można łatwo się odnieść. Jeśli jednak klucze zdalne nie odpowiadają poszczególnym elementom, musisz przechowywać je oddzielnie i zarządzać nimi za pomocą metody load()
.
W tej sekcji opisujemy, jak zbierać, przechowywać i aktualizować klucze zdalne, które nie są przechowywane w poszczególnych elementach.
Klucze elementów
W tej sekcji opisujemy, jak korzystać z kluczy zdalnych odpowiadających poszczególnym elementom. Zwykle, gdy klucze interfejsu API łączą się z poszczególnymi elementami, identyfikator elementu jest przekazywany jako parametr zapytania. Nazwa parametru wskazuje, czy serwer powinien odpowiadać z elementami przed podanym identyfikatorem czy po nim. W przykładowej klasie modelu User
pole id
z serwera jest używane jako klucz zdalny podczas wysyłania żądań dotyczących dodatkowych danych.
Gdy metoda load()
musi zarządzać kluczami zdalnymi dotyczącymi konkretnego produktu, klucze te są zwykle identyfikatorami danych pobranych z serwera. Operacje odświeżania nie wymagają klucza wczytywania, ponieważ pobierają tylko najnowsze dane.
Podobnie operacje dołączania na początku nie muszą pobierać żadnych dodatkowych danych, ponieważ odświeżanie zawsze pobiera najnowsze dane z serwera.
Operacje dołączania wymagają jednak identyfikatora. Wymaga to wczytania ostatniego elementu z bazy danych, a następnie użycia jego identyfikatora do wczytania następnej strony z danymi. Jeśli w bazie danych nie ma żadnych elementów, endOfPaginationReached
ma wartość Prawda, co wskazuje, że wymagane jest odświeżenie 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
W tej sekcji opisujemy, jak korzystać z kluczy zdalnych, które nie odpowiadają poszczególnym elementom.
Dodaj tabelę kluczy zdalnych
Gdy klucze zdalne nie są bezpośrednio powiązane z elementami listy, najlepiej przechowywać je w osobnej tabeli w lokalnej bazie danych. Zdefiniuj pomieszczenie, które 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 encji 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); }
Wczytywanie przy użyciu kluczy zdalnych
Gdy Twoja metoda load()
musi zarządzać zdalnymi kluczami stron, musisz zdefiniować ją inaczej w porównaniu z podstawowym użyciem RemoteMediator
:
- Uwzględnij dodatkową właściwość, która odwołuje się do DAO w tabeli kluczy zdalnej.
- Aby określić, który klucz ma zostać wczytany w następnej kolejności, wyślij zapytanie do tabeli kluczy zdalnych zamiast używać
PagingState
. - Wstaw lub zapisz zwrócony klucz zdalny ze źródła danych sieci w uzupełnieniu danych stronicowanych.
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 Twoja aplikacja musi obsługiwać tylko odświeżanie sieci z góry listy, tak jak w poprzednich przykładach, RemoteMediator
nie musi definiować zachowania wczytywania na początku.
Jeśli jednak Twoja aplikacja musi obsługiwać stopniowe wczytywanie z sieci do lokalnej bazy danych, musisz umożliwić wznawianie podziału na strony od zakotwiczenia, czyli pozycji przewijania użytkownika. Implementacja PagingSource
pokoju zrobi to za Ciebie, ale jeśli nie korzystasz z sali, możesz to zrobić, zastępując wartość PagingSource.getRefreshKey()
.
Przykład implementacji getRefreshKey()
znajdziesz w sekcji Definiowanie elementu PagingSource.
Rysunek 4 przedstawia proces wczytywania danych najpierw z lokalnej bazy danych, a następnie z sieci po jej wyczerpaniu.
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 jest wyświetlany, gdy JavaScript jest wyłączony
- Wczytywanie i wyświetlanie danych z podziałem na strony
- Testowanie implementacji stronicowania
- Migrate to Paging 3 (Migracja do strony 3)