Ağ ve veritabanı sayfası

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ü

Şekil 1. PagingSource ve PagingData ile Sayfalamanın yaşam döngüsü şeması.

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.

Şekil 2. PagingSource ve RemoteMediator ile Sayfalama'nın yaşam döngüsü şeması.

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, ağdan veritabanına veri yükler ve PagingSource, veritabanından veri yükler. Çağrı Cihazı, sayfalandırılmış verileri yüklemek için hem RemoteMediator hem de PagingSource'u kullanır.
Şekil 3. Katmanlı veri kaynağı kullanan bir Sayfalandırma uygulamasının şeması.

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, bir Pager 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:

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:

  1. Yükleme türüne ve o ana kadar yüklenen verilere bağlı olarak ağdan hangi sayfanın yükleneceğini belirleyin.
  2. Ağ isteğini tetikleyin.
  3. 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.

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. Uzak APPEND veya PREPEND yüklemeleri, devam etmeden önce REFRESH 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'dan PagingSource nesnesi döndüren sorgu yöntemini sağlamanız gerekir.
  • remoteMediator parametresi olarak RemoteMediator 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.

PagingSource, veritabanında veri tükenene kadar veritabanından kullanıcı arayüzüne yüklenir. Daha sonra RemoteMediator, ağdan veritabanına yüklenir ve sonrasında PagingSource yüklenmeye devam eder.
Şekil 4. PagingSource ve RemoteMediator'ın verileri yüklemek için birlikte nasıl çalıştığını gösteren şema.

Ek kaynaklar

Sayfalama kitaplığı hakkında daha fazla bilgi edinmek için aşağıdaki ek kaynaklara bakın:

Codelab uygulamaları

Sana Özel