Uygulamanızın, ağ bağlantıları güvenilir olmadığında veya kullanıcı çevrimdışı olduğunda kullanılabilmesini sağlayarak daha iyi bir kullanıcı deneyimi sağlayın. Bunu yapmanın bir yolu, aynı anda hem ağdan hem de yerel bir veritabanından sayfaya gitmektir. Bu şekilde, uygulamanız kullanıcı arayüzünü yerel veritabanı önbelleğinden alır ve yalnızca veritabanında başka veri kalmadığında ağa istek yapar.
Bu kılavuzda, Oda kalıcılık kitaplığı ve Sayfalama kitaplığının temel kullanımı hakkında bilgi sahibi olduğunuz varsayılır.
Veri yüklemelerini koordine etme
Sayfalandırma kitaplığı bu kullanım alanı için RemoteMediator
bileşenini sağlar. RemoteMediator
, uygulamanın önbelleğe alınan verileri tükendiğinde Çağrı kitaplığından sinyal olarak kullanılır. Bu sinyali, ağdan ek veri yüklemek ve yerel veritabanında depolamak için kullanabilirsiniz. Yerel veritabanında PagingSource
bu veriyi yükleyip görüntülenmesi için kullanıcı arayüzüne sağlayabilir.
Ek veri gerektiğinde, Çağrı kitaplığı RemoteMediator
uygulamasından load()
yöntemini çağırır. Bu, askıya alma işlevi olduğundan uzun süreli çalışmalarda
güvenlidir. Bu işlev genellikle yeni verileri bir ağ kaynağından alıp yerel depolama alanına kaydeder.
Bu süreç yeni verilerle çalışır ancak zaman içinde veritabanında depolanan veriler, kullanıcının manuel olarak bir yenilemeyi tetiklediğinde olduğu gibi geçersiz kılmayı gerektirir. Bu, load()
yöntemine iletilen LoadType
mülkü ile temsil edilir. LoadType
, mevcut verileri yenilemesi veya mevcut listeye eklenmesi ya da başa eklenmesi gereken ek verileri getirmesi gerekip gerekmediğiniRemoteMediator
bildirir.
Böylece RemoteMediator
, uygulamanızın, kullanıcıların görmek istedikleri verileri uygun sırada yüklemesini sağlar.
Sayfalama yaşam döngüsü
![](https://developer.android.google.cn/static/topic/libraries/architecture/images/paging3-base-lifecycle.png?authuser=5&hl=tr)
PagingSource
, doğrudan ağdan sayfalama yaparken verileri yükler ve bir LoadResult
nesnesi döndürür. PagingSource
uygulaması, pagingSourceFactory
parametresi aracılığıyla Pager
öğesine iletilir.
Kullanıcı arayüzü yeni veriler gerektirdiği için Pager
, PagingSource
üzerinden load()
yöntemini çağırır ve yeni verileri içeren PagingData
nesne akışını döndürür. Her PagingData
nesnesi genellikle, görüntülenmek üzere kullanıcı arayüzüne gönderilmeden önce ViewModel
içinde önbelleğe alınır.
![](https://developer.android.google.cn/static/topic/libraries/architecture/images/paging3-layered-lifecycle.png?authuser=5&hl=tr)
RemoteMediator
bu veri akışını değiştirir. PagingSource
, verileri yüklemeye devam eder ancak sayfa verilen veriler bittiğinde Sayfalama kitaplığı, ağ kaynağından yeni veriler yüklemesi için RemoteMediator
politikasını tetikler. RemoteMediator
, yeni verileri yerel veritabanında depoladığından ViewModel
içinde bellek içi önbelleğin bulunması gerekmez. Son olarak PagingSource
kendini geçersiz kılar ve Pager
, yeni verileri veritabanından yüklemek için yeni bir örnek oluşturur.
Temel kullanım
Uygulamanızın, öğe içeren bir ağ veri kaynağından User
öğe içeren sayfaları Oda veritabanında depolanan yerel bir önbelleğe yüklemesini istediğinizi varsayalım.
RemoteMediator
uygulaması, sayfalandırılmış verilerin ağdan veritabanına yüklenmesine yardımcı olur ancak verileri doğrudan kullanıcı arayüzüne yüklemez. Bunun yerine, uygulama veri kaynağı olarak veritabanını kullanır. Diğer bir deyişle, uygulama yalnızca veritabanında önbelleğe alınan verileri gösterir. PagingSource
uygulaması (örneğin, Room tarafından oluşturulan bir uygulama), önbelleğe alınan verilerin veritabanından kullanıcı arayüzüne yüklenmesini işler.
Oda varlıkları oluşturma
İlk adım, ağ veri kaynağından sayfa bölümü alınan verilerin yerel önbelleğini tutan bir veritabanı tanımlamak için Oda kalıcı kitaplığını kullanmaktır. Oda'yı kullanarak verileri yerel bir veritabanına kaydetme bölümünde açıklandığı gibi RoomDatabase
uygulamasını uygulayarak başlayın.
Daha sonra, Oda varlıklarını kullanarak verileri tanımlama bölümünde açıklandığı gibi liste öğeleri tablosunu temsil edecek bir Oda varlığı tanımlayın.
Bu alana, birincil anahtar olarak bir id
alanı ve liste öğelerinizin içerdiği diğer bilgiler için alanlar verin.
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; }
Oda DAO'larını kullanarak verilere erişme bölümünde açıklandığı gibi, bu Oda varlığı için bir veri erişim nesnesi (DAO) da tanımlamanız gerekir. Liste öğesi varlığının DAO'su aşağıdaki yöntemleri içermelidir:
- Tabloya öğe listesi ekleyen bir
insertAll()
yöntemi. - Sorgu dizesini parametre olarak alan ve sonuç listesi için bir
PagingSource
nesnesi döndüren yöntem. Bu şekilde, birPager
nesnesi bu tabloyu sayfa bölümü içeren veri kaynağı olarak kullanabilir. - Tablodaki tüm verileri silen bir
clearAll()
yöntemi.
Kotlin
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertAll(users: List<User>) @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> @Query("DELETE FROM users") suspend fun clearAll() }
Java
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List<User> users); @Query("SELECT * FROM users WHERE mLabel LIKE :query") PagingSource<Integer, User> pagingSource(String query); @Query("DELETE FROM users") int clearAll(); }
Java
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) void insertAll(List<User> users); @Query("SELECT * FROM users WHERE mLabel LIKE :query") PagingSource<Integer, User> pagingSource(String query); @Query("DELETE FROM users") int clearAll(); }
RemoteMediator Uygulama
RemoteMediator
uygulamasının ana rolü, Pager
öğesinde veri kalmadığında veya mevcut veriler geçersiz hale geldiğinde ağdan daha fazla veri yüklemektir. Yükleme davranışını tanımlamak için geçersiz kılmanız gereken bir load()
yöntemi içerir.
Tipik bir RemoteMediator
uygulaması aşağıdaki parametreleri içerir:
query
: Arka uç hizmetinden hangi verilerin alınacağını tanımlayan sorgu dizesi.database
: Yerel önbellek görevi gören Oda veritabanı.networkService
: Arka uç hizmeti için bir API örneği.
RemoteMediator<Key, Value>
uygulaması oluşturun. Key
türü ve Value
türü, aynı ağ veri kaynağına karşı PagingSource
tanımlarken kullanılacak olanla aynı olmalıdır. Tür parametrelerinin seçimi hakkında daha fazla bilgi için Anahtar ve değer türlerini seçme bölümüne bakın.
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 ) { ... } }
load()
yöntemi, yedek veri kümesinin güncellenmesinden ve PagingSource
öğesinin geçersiz kılınmasından sorumludur. Sayfa oluşturmayı destekleyen bazı kitaplıklar (Oda gibi), uyguladıkları PagingSource
nesnelerini geçersiz kılma işlemlerini otomatik olarak işler.
load()
yöntemi iki parametre alır:
- Şu ana kadar yüklenen sayfalar, en son erişilen dizin ve sayfalama akışını başlatmak için kullandığınız
PagingConfig
nesnesi ile ilgili bilgileri içerenPagingState
. LoadType
: Yükün türünü gösterir:REFRESH
,APPEND
veyaPREPEND
.
load()
yönteminin döndürülen değeri MediatorResult
nesnesidir. MediatorResult
, MediatorResult.Error
(hata açıklamasını içerir) veya MediatorResult.Success
(yüklenecek daha fazla veri olup olmadığını belirten bir sinyal içerir) olabilir.
load()
yöntemi aşağıdaki adımları gerçekleştirmelidir:
- Yükleme türüne ve o ana kadar yüklenen verilere bağlı olarak ağdan hangi sayfanın yükleneceğini belirleyin.
- Ağ isteğini tetikleyin.
- Yükleme işleminin sonucuna bağlı olarak işlemler gerçekleştirin:
- Yükleme başarılı olursa ve alınan öğeler listesi boş değilse liste öğelerini veritabanında depolayın ve
MediatorResult.Success(endOfPaginationReached = false)
değerini döndürün. Veriler depolandıktan sonra, yeni verilerin Sayfalama kitaplığına bildirimde bulunmak için veri kaynağını geçersiz kılın. - Yükleme başarılı olursa ve alınan öğe listesi boşsa veya bu son sayfa diziniyse
MediatorResult.Success(endOfPaginationReached = true)
değerini döndürür. Veriler depolandıktan sonra, yeni veriler için Sayfalama kitaplığına bildirim göndermek için veri kaynağını geçersiz kılın. - İstek bir hataya neden olursa
MediatorResult.Error
değerini döndürün.
- Yükleme başarılı olursa ve alınan öğeler listesi boş değilse liste öğelerini veritabanında depolayın ve
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 ); }
Başlatma yöntemini tanımlama
RemoteMediator
uygulamaları, önbelleğe alınan verilerin güncel olup olmadığını kontrol etmek ve uzaktan yenilemenin tetiklenip tetiklenmeyeceğine karar vermek için initialize()
yöntemini de geçersiz kılabilir. Bu yöntem, herhangi bir yükleme yapılmadan önce çalışır. Bu sayede, yerel veya uzaktan yüklemeleri tetiklemeden önce veritabanını değiştirebilirsiniz (örneğin, eski verileri temizlemek için).
initialize()
eşzamansız bir işlev olduğundan veritabanındaki mevcut verilerin alaka düzeyini belirlemek için veri yükleyebilirsiniz. En yaygın durum, önbelleğe alınan verilerin yalnızca belirli bir süre için geçerli olmasıdır. RemoteMediator
, bu geçerlilik süresinin geçip geçmediğini kontrol edebilir. Bu durumda, Çağrı kitaplığının verileri tamamen yenilemesi gerekir. initialize()
uygulamaları aşağıdaki gibi bir InitializeAction
döndürmelidir:
- Yerel verilerin tamamen yenilenmesi gereken durumlarda
initialize()
,InitializeAction.LAUNCH_INITIAL_REFRESH
değerini döndürmelidir. Bu işlem,RemoteMediator
cihazının verileri tamamen yeniden yüklemek için uzaktan yenileme yapmasına neden olur. UzakAPPEND
veyaPREPEND
yüklemeleri, devam etmeden önceREFRESH
yüklemesinin başarılı olmasını bekler. - Yerel verilerin yenilenmesi gerekmediği durumlarda
initialize()
,InitializeAction.SKIP_INITIAL_REFRESH
değerini döndürmelidir. Bu durum,RemoteMediator
cihazının uzaktan yenilemeyi atlamasına ve önbelleğe alınan verileri yüklemesine neden olur.
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); }
Çağrı Cihazı Oluşturma
Son olarak, sayfa bölümü verilen veri akışını ayarlamak için bir Pager
örneği oluşturmanız gerekir.
Bu, basit bir ağ veri kaynağından Pager
oluşturmaya benzer ancak farklı olarak yapmanız gereken iki şey vardır:
- Bir
PagingSource
oluşturucusunu doğrudan iletmek yerine, DAO'danPagingSource
nesnesi döndüren sorgu yöntemini sağlamanız gerekir. remoteMediator
parametresi olarakRemoteMediator
uygulamanızın bir örneğini sağlamanız gerekir.
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));
Yarış koşullarını yönetme
Birden fazla kaynaktan veri yüklerken uygulamanızın ele alması gereken durumlardan biri, önbelleğe alınan yerel verilerin uzak veri kaynağıyla senkronize olmamasıdır.
RemoteMediator
uygulamanızdaki initialize()
yöntemi LAUNCH_INITIAL_REFRESH
sonucunu döndürdüğünde veriler güncelliğini yitirir ve yeni verilerle değiştirilmesi gerekir. Tüm PREPEND
veya APPEND
yükleme istekleri, REFRESH
uzaktan yüklemesinin başarılı olması için beklemeye zorlanır. PREPEND
veya APPEND
istekleri REFRESH
isteğinden önce sıraya alındığından, bu yükleme çağrılarına iletilen PagingState
istekleri, çalıştıkları zaman güncelliğini yitirmiş olabilir.
Verilerin yerel olarak nasıl depolandığına bağlı olarak, önbelleğe alınan verilerde yapılan değişiklikler geçersiz kılmaya ve yeni veri getirmelere neden olursa uygulamanız gereksiz istekleri yok sayabilir.
Örneğin Oda, herhangi bir veri eklemedeki sorguları geçersiz kılar. Diğer bir deyişle, veritabanına yeni veriler eklendiğinde, bekleyen yük isteklerine yenilenmiş verilere sahip yeni PagingSource
nesneleri sağlanır.
Bu veri senkronizasyonu sorununu çözmek, kullanıcıların en alakalı ve güncel verileri görmesini sağlamak için çok önemlidir. En iyi çözüm çoğunlukla ağ veri kaynağının verileri sayfa yapma şekline bağlıdır. Her durumda, uzaktan tuşlar sunucudan istenen en son sayfa hakkındaki bilgileri kaydetmenize olanak tanır. Uygulamanız, daha sonra yüklenecek doğru veri sayfasını belirlemek ve istemek için bu bilgileri kullanabilir.
Uzak anahtarları yönet
Uzak anahtarlar, bir RemoteMediator
uygulamasının, arka uç hizmetine hangi verilerin yükleneceğini bildirmek için kullandığı anahtarlardır. En basit şekilde, sayfalan her veri öğesinde kolayca başvurabileceğiniz bir uzak anahtar bulunur. Bununla birlikte, uzaktan anahtarlar tek tek öğelere karşılık gelmiyorsa bunları ayrı olarak depolamanız ve load()
yönteminizde yönetmeniz gerekir.
Bu bölümde, ayrı ayrı öğelerde depolanmayan uzak anahtarların nasıl toplanacağı, depolanacağı ve güncelleneceği açıklanmaktadır.
Öğe anahtarları
Bu bölümde her bir öğeye karşılık gelen uzak anahtarlarla nasıl çalışılacağı açıklanmaktadır. Tipik olarak, bir API anahtarı tek tek öğelerden çıktığında öğe kimliği bir sorgu parametresi olarak iletilir. Parametre adı, sunucunun sağlanan kimlikten önce mi sonra mı yoksa öğelerle yanıt verip vermeyeceğini belirtir. User
model sınıfı örneğinde, sunucudaki id
alanı ek veri istenirken uzak anahtar olarak kullanılır.
load()
yönteminizin öğeye özel uzak anahtarları yönetmesi gerektiğinde, bu anahtarlar genellikle sunucudan getirilen verilerin kimlikleridir. Yenileme işlemleri yalnızca en son verileri aldıkları için yükleme anahtarına ihtiyaç duymaz.
Benzer şekilde, yenileme her zaman sunucudan en yeni verileri aldığından, başa ekleme işlemlerinin ek veri getirmesine gerek yoktur.
Ancak, ekleme işlemleri kimlik gerektirir. Bu işlem, veritabanındaki son öğeyi yüklemenizi ve sonraki veri sayfasını yüklemek için öğenin kimliğini kullanmanızı gerektirir. Veritabanında hiç öğe yoksa endOfPaginationReached
, verilerin yenilenmesi gerektiğini belirten "true" (doğru) değerine ayarlanır.
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); }
Sayfa anahtarları
Bu bölümde, bağımsız öğelere karşılık gelmeyen uzak anahtarlarla nasıl çalışılacağı açıklanmaktadır.
Uzak anahtar tablosu ekle
Uzak anahtarlar liste öğeleriyle doğrudan ilişkilendirilmediğinde, bunları yerel veritabanında ayrı bir tabloda depolamak en iyisidir. Uzak anahtarlar tablosunu temsil eden bir Oda varlığı tanımlayın:
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; }
Ayrıca RemoteKey
varlığı için bir DAO tanımlamanız gerekir:
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); }
Uzak anahtarlarla yükle
load()
yönteminizin uzak sayfa anahtarlarını yönetmesi gerektiğinde, bu yöntemi RemoteMediator
ürününün temel kullanımına kıyasla aşağıdaki şekillerde farklı bir şekilde tanımlamanız gerekir:
- Uzak anahtar tablonuz için DAO'ya referans içeren ek bir özellik ekleyin.
PagingState
kullanmak yerine uzak anahtar tablosunu sorgulayarak bir sonraki adımda hangi anahtarı yükleyeceğinizi belirleyin.- Sayfalandırılmış verilerin kendisine ek olarak ağ veri kaynağından döndürülen uzak anahtarı ekleyin veya depolayın.
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); }
Yerinde yenile
Uygulamanızın, önceki örneklerde olduğu gibi yalnızca listenin üst kısmından ağ yenilemelerini desteklemesi gerekiyorsa RemoteMediator
öğenizin başına yükleme davranışını tanımlaması gerekmez.
Ancak uygulamanızın ağdan yerel veritabanına artımlı yüklemeyi desteklemesi gerekiyorsa kullanıcının kaydırma konumu olan sabitleyiciden başlayarak sayfalara ayırma işlemini devam ettirme desteği sağlamalısınız. Oda PagingSource
uygulaması bu işlemi sizin için halleder ancak Oda kullanmıyorsanız bu işlemi PagingSource.getRefreshKey()
özelliğini geçersiz kılarak yapabilirsiniz.
getRefreshKey()
öğesinin örnek bir uygulaması için PagingSource'u tanımlama bölümüne bakın.
Şekil 4'te, önce yerel veritabanından ve daha sonra veritabanında veriler tükendiğinde ağdan veri yükleme işlemi gösterilmiştir.
Ek kaynaklar
Sayfalama kitaplığı hakkında daha fazla bilgi edinmek için aşağıdaki ek kaynaklara bakın:
Codelab uygulamaları
Sana Özel
Sizin için önerilenler
- Not: Bağlantı metni JavaScript kapalıyken gösterilir
- Sayfalı verileri yükleme ve görüntüleme
- Sayfalama uygulamanızı test etme
- 3. Sayfaya Geçiş