Migliora l'esperienza utente assicurandoti che la tua app possa essere utilizzata quando le connessioni di rete non sono affidabili o quando l'utente è offline. Un modo per farlo è cercare pagine dalla rete e da un database locale contemporaneamente. In questo modo, l'app utilizza l'interfaccia utente dalla cache di un database locale e invia richieste alla rete solo quando il database non contiene altri dati.
Questa guida presuppone la conoscenza della libreria di persistenza della stanza e dell'utilizzo di base della libreria di paging.
Caricamenti dati di Coordinate
La libreria di paging fornisce il componente RemoteMediator
per questo caso d'uso. RemoteMediator
funge da indicatore dalla libreria di paging quando l'app ha esaurito i dati memorizzati nella cache. Puoi utilizzare questo indicatore per caricare dati aggiuntivi dalla rete e archiviarli nel database locale, dove un elemento PagingSource
può caricarli e fornirli alla UI da visualizzare.
Se sono necessari dati aggiuntivi, la libreria Paging chiama il metodo load()
dall'implementazione RemoteMediator
. Questa è una funzione di sospensione,
quindi è sicuro eseguire lavori di lunga durata. Questa funzione in genere recupera i nuovi dati da
un'origine di rete e li salva nello spazio di archiviazione locale.
Questo processo funziona con i nuovi dati, ma nel tempo i dati archiviati nel database richiedono l'annullamento della convalida, ad esempio quando l'utente attiva manualmente un aggiornamento. Ciò è rappresentato dalla proprietà LoadType
passata al metodo load()
. LoadType
indica all'RemoteMediator
se è necessario aggiornare i dati esistenti o recuperare dati aggiuntivi da aggiungere o anteporre all'elenco esistente.
In questo modo, RemoteMediator
assicura che la tua app carichi nell'ordine appropriato i dati che gli utenti vogliono vedere.
Ciclo di vita delle pagine
Quando si naviga direttamente dalla rete, PagingSource
carica i dati e restituisce un oggetto LoadResult
. L'implementazione PagingSource
viene passata al parametro
Pager
tramite il
parametro pagingSourceFactory
.
Poiché l'interfaccia utente richiede nuovi dati, Pager
chiama il metodo
load()
da
PagingSource
e restituisce un flusso di oggetti
PagingData
che
incapsulano i nuovi dati. In genere, ogni oggetto PagingData
viene memorizzato nella cache all'interno di ViewModel
prima di essere inviato all'interfaccia utente per la visualizzazione.
RemoteMediator
modifica questo flusso di dati. Un PagingSource
carica comunque i dati;
ma quando i dati impaginati sono esauriti, la libreria di paging attiva il
RemoteMediator
per caricare nuovi dati dall'origine della rete. RemoteMediator
archivia i nuovi dati nel database locale, quindi non è necessaria una cache in memoria
in ViewModel
. Infine, l'elemento PagingSource
viene invalidato e l'elemento Pager
crea una nuova istanza per caricare i dati aggiornati dal database.
Utilizzo di base
Supponi di volere che la tua app carichi pagine di elementi User
da un'origine dati di rete con chiave elemento in una cache locale archiviata in un database Room.
Un'implementazione RemoteMediator
consente di caricare nel database i dati impaginati della rete, ma non di caricare i dati direttamente nell'interfaccia utente. L'app utilizza invece il database come fonte attendibile. In altre parole, l'app mostra solo
i dati che sono stati memorizzati nella cache del database. Un'implementazione di PagingSource
(ad esempio quella generata da una stanza virtuale) gestisce il caricamento dei dati memorizzati nella cache dal database nell'interfaccia utente.
Creare entità stanza
Il primo passaggio consiste nell'utilizzare la libreria di persistenza della stanza per definire un database in cui è memorizzata una cache locale dei dati di impaginazione provenienti dall'origine dati della rete. Inizia con un'implementazione di RoomDatabase
come descritto in Salvare i dati in un database locale utilizzando la stanza virtuale.
Successivamente, definisci un'entità Room per rappresentare una tabella di elementi elenco, come descritto in
Definire i dati utilizzando le entità Room.
Assegna al campo id
come chiave primaria e campi per qualsiasi altra informazione contenuta negli elementi dell'elenco.
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; }
Devi inoltre definire un oggetto di accesso ai dati (DAO) per questa entità Room come descritto in Accesso ai dati utilizzando i DAO delle stanze. Il DAO dell'entità dell'elemento elenco deve includere i seguenti metodi:
- Un metodo
insertAll()
che inserisce un elenco di elementi nella tabella. - Un metodo che prende la stringa di query come parametro e restituisce un oggetto
PagingSource
per l'elenco dei risultati. In questo modo, un oggettoPager
può utilizzare questa tabella come origine di dati impaginati. - Un metodo
clearAll()
che elimina tutti i dati della tabella.
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(); }
Implementare un RemoteMediator
Il ruolo principale di RemoteMediator
è caricare più dati dalla rete quando
Pager
esaurisce i dati o i dati esistenti vengono invalidati. Include un metodo load()
di cui devi eseguire l'override per definire il comportamento di caricamento.
Una tipica implementazione RemoteMediator
include i seguenti parametri:
query
: una stringa di query che definisce i dati da recuperare dal servizio di backend.database
: il database della stanza che funge da cache locale.networkService
: un'istanza API per il servizio di backend.
Crea un'implementazione RemoteMediator<Key, Value>
. Il tipo Key
e il tipo
Value
devono essere uguali a come lo sarebbero se definissi un
PagingSource
in base alla stessa origine dati di rete. Per ulteriori informazioni sulla selezione dei parametri di tipo, consulta Selezionare tipi di chiave e valore.
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 ) { ... } }
Il metodo load()
è responsabile dell'aggiornamento del set di dati di supporto e dell'annullamento di PagingSource
. Alcune librerie che supportano il paging (come Room)
gestiscono automaticamente gli oggetti PagingSource
non validi
implementati.
Il metodo load()
accetta due parametri:
PagingState
, che contiene informazioni sulle pagine caricate finora, sull'indice a cui si è eseguito l'ultimo accesso e sull'oggettoPagingConfig
che hai utilizzato per inizializzare il flusso di paging.LoadType
, che indica il tipo di carico:REFRESH
,APPEND
oPREPEND
.
Il valore restituito del metodo load()
è un oggetto MediatorResult
. MediatorResult
può essere
MediatorResult.Error
(che include la descrizione dell'errore) o
MediatorResult.Success
(che include un indicatore che indica se ci sono o meno dati da caricare).
Il metodo load()
deve eseguire i seguenti passaggi:
- Stabilisci quale pagina caricare dalla rete in base al tipo di caricamento e ai dati caricati finora.
- Attiva la richiesta di rete.
- Esegui azioni in base al risultato dell'operazione di caricamento:
- Se il caricamento ha esito positivo e l'elenco di elementi ricevuto non è vuoto, archivia gli elementi dell'elenco nel database e restituisci
MediatorResult.Success(endOfPaginationReached = false)
. Dopo l'archiviazione dei dati, invalida l'origine dati per notificare i nuovi dati alla libreria Paging. - Se il caricamento ha esito positivo e l'elenco di elementi ricevuto è vuoto o è l'ultimo indice di pagina, restituisce
MediatorResult.Success(endOfPaginationReached = true)
. Dopo aver archiviato i dati, annulla la validità dell'origine dati per notificare i nuovi dati alla libreria Paging. - Se la richiesta genera un errore, restituisci
MediatorResult.Error
.
- Se il caricamento ha esito positivo e l'elenco di elementi ricevuto non è vuoto, archivia gli elementi dell'elenco nel database e restituisci
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 ); }
Definisci il metodo di inizializzazione
Le implementazioni RemoteMediator
possono anche sostituire il metodo
initialize()
per verificare se i dati memorizzati nella cache sono obsoleti e decidere se attivare
un aggiornamento remoto. Questo metodo viene eseguito prima dell'esecuzione di qualsiasi caricamento, in modo da poter manipolare il database (ad esempio per cancellare i dati precedenti) prima di attivare qualsiasi caricamento locale o remoto.
Poiché initialize()
è una funzione asincrona, puoi caricare dati per determinare la pertinenza dei dati esistenti nel database. Il caso più comune è che i dati memorizzati nella cache sono validi solo per un determinato periodo di tempo. RemoteMediator
può controllare se questa scadenza è trascorsa, nel qual caso la libreria Paging deve aggiornare completamente i dati. Le implementazioni di
initialize()
dovrebbero restituire un InitializeAction
come segue:
- Nei casi in cui i dati locali debbano essere completamente aggiornati,
initialize()
dovrebbe restituireInitializeAction.LAUNCH_INITIAL_REFRESH
. Questo fa sì cheRemoteMediator
esegua un aggiornamento remoto per ricaricare completamente i dati. Prima di procedere, tutti i caricamenti remotiAPPEND
oPREPEND
attendono il completamento del caricamentoREFRESH
. - Nei casi in cui i dati locali non debbano essere aggiornati,
initialize()
deve restituireInitializeAction.SKIP_INITIAL_REFRESH
. In questo modoRemoteMediator
salterà l'aggiornamento remoto e caricherà i dati memorizzati nella cache.
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); }
Crea un cercapersone
Infine, devi creare un'istanza Pager
per configurare lo stream di dati impaginati.
È simile alla creazione di un Pager
da una semplice origine dati di rete, ma ci sono due cose che devi fare in modo diverso:
- Anziché passare direttamente un costruttore
PagingSource
, devi fornire il metodo di query che restituisce un oggettoPagingSource
dal DAO. - Devi fornire un'istanza dell'implementazione
RemoteMediator
come parametroremoteMediator
.
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));
Gestire le gare di gara
Una situazione che l'app deve gestire quando carica dati da più origini è quella in cui i dati memorizzati nella cache locale non siano sincronizzati con l'origine dati remota.
Quando il metodo initialize()
dell'implementazione RemoteMediator
restituisce LAUNCH_INITIAL_REFRESH
, i dati sono obsoleti e devono essere sostituiti con dati aggiornati. Eventuali richieste di caricamento PREPEND
o APPEND
devono attendere il completamento del caricamento remoto
REFRESH
. Poiché le richieste PREPEND
o APPEND
sono state
in coda prima della richiesta REFRESH
, è possibile che il valore PagingState
passato a queste chiamate al caricamento non sia aggiornato al momento dell'esecuzione.
A seconda di come i dati sono archiviati localmente, la tua app può ignorare le richieste ridondanti se le modifiche ai dati memorizzati nella cache causano l'annullamento della validità e il recupero di nuovi dati.
Ad esempio, Room non rende valide le query relative all'inserimento di dati. Ciò significa che i nuovi oggetti PagingSource
con i dati aggiornati vengono forniti alle richieste di caricamento in attesa quando vengono inseriti nuovi dati nel database.
La risoluzione di questo problema di sincronizzazione dei dati è essenziale per garantire che gli utenti vedano i dati più pertinenti e aggiornati. La soluzione migliore dipende principalmente dal modo in cui l'origine dati di rete pagina i dati. In ogni caso, le chiavi remote ti consentono di salvare le informazioni relative alla pagina più recente richiesta dal server. Puoi usare queste informazioni per identificare e richiedere la pagina di dati corretta da caricare in seguito.
Gestisci chiavi remoti
Le chiavi remote sono chiavi utilizzate da un'implementazione RemoteMediator
per indicare al servizio di backend i dati da caricare successivamente. Nel caso più semplice, ogni elemento di dati impaginati include una chiave remota a cui puoi facilmente fare riferimento. Tuttavia, se le chiavi remote non corrispondono a singoli elementi, devi archiviarle separatamente e gestirle nel tuo metodo load()
.
Questa sezione descrive come raccogliere, archiviare e aggiornare le chiavi remote che non sono archiviate in singoli elementi.
Chiavi elemento
Questa sezione descrive come utilizzare i tasti del telecomando che corrispondono a singoli elementi. In genere, quando un'API si basa su singoli elementi, l'ID elemento viene trasmesso come parametro di ricerca. Il nome del parametro indica se il server deve rispondere con elementi prima o dopo l'ID fornito. Nell'esempio della classe del modello User
, il campo id
del server viene utilizzato come chiave remota quando vengono richiesti dati aggiuntivi.
Quando il metodo load()
deve gestire chiavi remote specifiche degli elementi, queste chiavi
sono in genere gli ID dei dati recuperati dal server. Le operazioni di aggiornamento non richiedono una chiave di caricamento, perché recuperano solo i dati più recenti.
Allo stesso modo, le operazioni di anteposta non richiedono il recupero di dati aggiuntivi, poiché refresh estrae sempre i dati più recenti dal server.
Tuttavia, le operazioni di accodamento richiedono un ID. Ciò richiede di caricare l'ultimo elemento del database e di utilizzarne l'ID per caricare la pagina successiva di dati. Se non ci sono elementi nel database, endOfPaginationReached
è impostato su true, a indicare che è necessario un aggiornamento dei dati.
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); }
Chiavi pagina
Questa sezione descrive come utilizzare i tasti del telecomando che non corrispondono a singoli elementi.
Aggiungi tabella chiavi remota
Quando le chiavi remote non sono associate direttamente agli elementi dell'elenco, è preferibile archiviarle in una tabella separata nel database locale. Definisci un'entità Room che rappresenta una tabella di chiavi remote:
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; }
Devi anche definire un DAO per l'entità 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); }
Carica con tasti remoti
Quando il metodo load()
deve gestire le chiavi delle pagine remote, devi definirlo in modo diverso nei seguenti modi rispetto all'utilizzo di base di RemoteMediator
:
- Includi un'ulteriore proprietà che contiene un riferimento al DAO per la tabella delle chiavi remote.
- Determina quale chiave caricare successivamente eseguendo una query sulla tabella delle chiavi remote anziché utilizzare
PagingState
. - Inserisci o memorizza la chiave remota restituita dall'origine dati di rete oltre ai dati impaginati stessi.
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); }
Aggiornamento attivo
Se la tua app deve supportare solo gli aggiornamenti di rete dall'inizio dell'elenco come
negli esempi precedenti, non è necessario che l'elemento RemoteMediator
definisca
il comportamento di caricamento anticipato.
Tuttavia, se la tua app deve supportare il caricamento incrementale dalla rete al database locale, devi fornire supporto per riprendere l'impaginazione a partire dall'ancoraggio, la posizione di scorrimento dell'utente. L'implementazione PagingSource
di Room si occupa di questo aspetto, ma se non utilizzi Room, puoi eseguire l'override di PagingSource.getRefreshKey()
.
Per un'implementazione di esempio di getRefreshKey()
, consulta Definire l'origine pagina.
La Figura 4 illustra il processo di caricamento dei dati prima dal database locale e poi dalla rete una volta che i dati del database sono esauriti.
Risorse aggiuntive
Per saperne di più sulla libreria Paging, consulta le seguenti risorse aggiuntive:
Codelab
Samples
Consigliato per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Caricare e visualizzare i dati impaginati
- Testare l'implementazione di Paging
- Eseguire la migrazione a Paging 3