صفحه از شبکه و پایگاه داده (Views)

مفاهیم و پیاده‌سازی Jetpack Compose

با اطمینان از اینکه برنامه شما می‌تواند در مواقعی که اتصالات شبکه غیرقابل اعتماد هستند یا کاربر آفلاین است، استفاده شود، یک تجربه کاربری بهبود یافته ارائه دهید. یک راه برای انجام این کار، صفحه‌بندی از شبکه و از یک پایگاه داده محلی به طور همزمان است. به این ترتیب، برنامه شما رابط کاربری را از حافظه پنهان پایگاه داده محلی هدایت می‌کند و فقط زمانی که داده دیگری در پایگاه داده وجود ندارد، درخواست‌هایی را به شبکه ارسال می‌کند.

این راهنما فرض می‌کند که شما با کتابخانه‌ی پایداری Room و نحوه‌ی استفاده‌ی اولیه از کتابخانه‌ی Paging آشنا هستید.

بارهای داده هماهنگ

کتابخانه Paging کامپوننت RemoteMediator را برای این مورد استفاده ارائه می‌دهد. RemoteMediator به عنوان سیگنالی از کتابخانه Paging عمل می‌کند، زمانی که داده‌های ذخیره شده در حافظه پنهان برنامه تمام شده باشد. می‌توانید از این سیگنال برای بارگیری داده‌های اضافی از شبکه و ذخیره آنها در پایگاه داده محلی استفاده کنید، جایی که یک PagingSource می‌تواند آن را بارگیری کرده و برای نمایش در اختیار رابط کاربری قرار دهد.

وقتی به داده‌های اضافی نیاز باشد، کتابخانه Paging متد load() را از پیاده‌سازی RemoteMediator فراخوانی می‌کند. این یک تابع معلق‌کننده است، بنابراین انجام کارهای طولانی‌مدت با آن ایمن است. این تابع معمولاً داده‌های جدید را از منبع شبکه دریافت کرده و در حافظه محلی ذخیره می‌کند.

این فرآیند با داده‌های جدید کار می‌کند، اما با گذشت زمان، داده‌های ذخیره شده در پایگاه داده نیاز به اعتبارسنجی دارند، مانند زمانی که کاربر به صورت دستی یک به‌روزرسانی را فعال می‌کند. این امر توسط ویژگی LoadType که به متد load() ارسال می‌شود، نشان داده می‌شود. LoadType به RemoteMediator اطلاع می‌دهد که آیا نیاز به به‌روزرسانی داده‌های موجود یا واکشی داده‌های اضافی که باید به لیست موجود اضافه یا در ابتدا اضافه شوند، دارد یا خیر.

به این ترتیب، RemoteMediator تضمین می‌کند که برنامه شما داده‌هایی را که کاربران می‌خواهند ببینند، به ترتیب مناسب بارگذاری کند.

چرخه حیات صفحه‌بندی

شکل ۱. نمودار چرخه حیات صفحه‌بندی با PagingSource و PagingData.

هنگام صفحه‌بندی مستقیم از شبکه، PagingSource داده‌ها را بارگذاری کرده و یک شیء LoadResult برمی‌گرداند. پیاده‌سازی PagingSource از طریق پارامتر pagingSourceFactory به Pager ارسال می‌شود.

همانطور که رابط کاربری به داده‌های جدید نیاز دارد، Pager متد load() را از PagingSource فراخوانی می‌کند و جریانی از اشیاء PagingData را برمی‌گرداند که داده‌های جدید را کپسوله‌سازی می‌کنند. هر شیء PagingData معمولاً قبل از ارسال به رابط کاربری برای نمایش، در ViewModel ذخیره می‌شود.

شکل ۲. نمودار چرخه حیات صفحه‌بندی با PagingSource و RemoteMediator.

RemoteMediator این جریان داده را تغییر می‌دهد. یک PagingSource همچنان داده‌ها را بارگذاری می‌کند؛ اما وقتی داده‌های صفحه‌بندی شده تمام می‌شوند، کتابخانه Paging، RemoteMediator برای بارگذاری داده‌های جدید از منبع شبکه فعال می‌کند. RemoteMediator داده‌های جدید را در پایگاه داده محلی ذخیره می‌کند، بنابراین یک حافظه پنهان در حافظه در ViewModel غیرضروری است. در نهایت، PagingSource خود را نامعتبر می‌کند و Pager یک نمونه جدید برای بارگذاری داده‌های تازه از پایگاه داده ایجاد می‌کند.

کاربرد اولیه

فرض کنید می‌خواهید برنامه‌تان صفحاتی از آیتم‌های User را از یک منبع داده شبکه با کلید آیتم در یک حافظه پنهان محلی ذخیره شده در پایگاه داده Room بارگذاری کند.

RemoteMediator داده‌ها را از شبکه به پایگاه داده بارگذاری می‌کند و PagingSource داده‌ها را از پایگاه داده بارگذاری می‌کند. یک Pager از هر دو RemoteMediator و PagingSource برای بارگذاری داده‌های صفحه‌بندی شده استفاده می‌کند.
شکل ۳. نمودار پیاده‌سازی صفحه‌بندی که از یک منبع داده لایه‌بندی شده استفاده می‌کند.

پیاده‌سازی RemoteMediator به بارگذاری داده‌های صفحه‌بندی‌شده از شبکه در پایگاه داده کمک می‌کند، اما داده‌ها را مستقیماً در رابط کاربری بارگذاری نمی‌کند. در عوض، برنامه از پایگاه داده به عنوان منبع حقیقت استفاده می‌کند. به عبارت دیگر، برنامه فقط داده‌هایی را نمایش می‌دهد که در پایگاه داده ذخیره شده‌اند. پیاده‌سازی PagingSource (به عنوان مثال، پیاده‌سازی تولید شده توسط Room) بارگذاری داده‌های ذخیره‌شده از پایگاه داده در رابط کاربری را مدیریت می‌کند.

ایجاد موجودیت‌های اتاق

اولین قدم استفاده از کتابخانه‌ی پایداری Room برای تعریف یک پایگاه داده است که یک حافظه‌ی نهان محلی از داده‌های صفحه‌بندی شده از منبع داده‌ی شبکه را در خود نگه می‌دارد. با پیاده‌سازی RoomDatabase همانطور که در بخش «ذخیره داده‌ها در یک پایگاه داده‌ی محلی با استفاده از Room» توضیح داده شده است، شروع کنید.

سپس، یک موجودیت Room تعریف کنید تا جدولی از آیتم‌های لیست را همانطور که در تعریف داده‌ها با استفاده از موجودیت‌های Room توضیح داده شده است، نمایش دهد. به آن یک فیلد id به عنوان کلید اصلی و همچنین فیلدهایی برای هر اطلاعات دیگری که آیتم‌های لیست شما شامل می‌شوند، اختصاص دهید.

جاوا

@Entity(tableName = "users")
public class User {
  public String id;
  public String label;
}

جاوا

@Entity(tableName = "users")
public class User {
  public String id;
  public String label;
}

شما همچنین باید یک شیء دسترسی به داده (DAO) برای این موجودیت Room همانطور که در بخش «دسترسی به داده‌ها با استفاده از DAOهای Room» توضیح داده شده است، تعریف کنید. DAO برای موجودیت آیتم لیست باید شامل متدهای زیر باشد:

  • یک متد insertAll() که لیستی از آیتم‌ها را در جدول درج می‌کند.
  • متدی که رشته پرس‌وجو را به عنوان پارامتر می‌گیرد و یک شیء PagingSource برای لیست نتایج برمی‌گرداند. به این ترتیب، یک شیء Pager می‌تواند از این جدول به عنوان منبع داده‌های صفحه‌بندی شده استفاده کند.
  • یک متد clearAll() که تمام داده‌های جدول را حذف می‌کند.

جاوا

@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();
}

جاوا

@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

نقش اصلی RemoteMediator بارگذاری داده‌های بیشتر از شبکه در زمانی است که Pager با کمبود داده مواجه شود یا داده‌های موجود نامعتبر شوند. این تابع شامل یک متد load() است که باید برای تعریف رفتار بارگذاری، آن را بازنویسی کنید.

یک پیاده‌سازی معمول RemoteMediator شامل پارامترهای زیر است:

  • query : یک رشته‌ی پرس‌وجو که مشخص می‌کند کدام داده‌ها از سرویس backend بازیابی شوند.
  • database : پایگاه داده Room که به عنوان یک حافظه پنهان محلی عمل می‌کند.
  • networkService : یک نمونه API برای سرویس backend.

یک پیاده‌سازی RemoteMediator<Key, Value> ایجاد کنید. نوع Key و نوع Value باید مشابه حالتی باشند که یک PagingSource برای همان منبع داده شبکه تعریف می‌کنید. برای اطلاعات بیشتر در مورد انتخاب پارامترهای نوع، به بخش «انتخاب انواع کلید و مقدار» مراجعه کنید.

جاوا

@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
  ) {
    ...
  }
}

جاوا

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() مسئول به‌روزرسانی مجموعه داده‌های پشتیبان و نامعتبر کردن PagingSource است. برخی از کتابخانه‌هایی که از صفحه‌بندی پشتیبانی می‌کنند (مانند Room) به‌طور خودکار نامعتبر کردن اشیاء PagingSource را که پیاده‌سازی می‌کنند، مدیریت می‌کنند.

متد load() دو پارامتر می‌گیرد:

  • PagingState ، که شامل اطلاعاتی در مورد صفحات بارگذاری شده تاکنون، آخرین ایندکس دسترسی یافته و شیء PagingConfig است که برای مقداردهی اولیه جریان صفحه‌بندی استفاده کرده‌اید.
  • LoadType که نوع بارگذاری را نشان می‌دهد: REFRESH ، APPEND یا PREPEND .

مقدار بازگشتی متد load() یک شیء MediatorResult است. MediatorResult می‌تواند MediatorResult.Error (که شامل شرح خطا است) یا MediatorResult.Success (که شامل سیگنالی است که بیان می‌کند آیا داده‌های بیشتری برای بارگذاری وجود دارد یا خیر) باشد.

متد load() باید مراحل زیر را انجام دهد:

  1. بسته به نوع بارگذاری و داده‌هایی که تاکنون بارگذاری شده‌اند، تعیین کنید که کدام صفحه از شبکه بارگذاری شود.
  2. درخواست شبکه را فعال کنید.
  3. بسته به نتیجه عملیات بارگذاری، اقدامات زیر را انجام دهید:
    • اگر بارگذاری موفقیت‌آمیز باشد و لیست دریافتی از آیتم‌ها خالی نباشد، آیتم‌های لیست را در پایگاه داده ذخیره کرده و MediatorResult.Success(endOfPaginationReached = false) را برمی‌گرداند. پس از ذخیره داده‌ها، منبع داده را نامعتبر کنید تا کتابخانه Paging از داده‌های جدید مطلع شود.
    • اگر بارگذاری موفقیت‌آمیز باشد و لیست دریافتی از آیتم‌ها خالی باشد یا آخرین ایندکس صفحه باشد، آنگاه MediatorResult.Success(endOfPaginationReached = true) را برگردانید. پس از ذخیره داده‌ها، منبع داده را نامعتبر کنید تا کتابخانه Paging از داده‌های جدید مطلع شود.
    • اگر درخواست باعث خطا شود، مقدار MediatorResult.Error را برمی‌گرداند.

جاوا

@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);
    });
}

جاوا

@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
  );
}

تعریف متد مقداردهی اولیه

پیاده‌سازی‌های RemoteMediator همچنین می‌توانند متد initialize() را برای بررسی اینکه آیا داده‌های ذخیره‌شده قدیمی هستند یا خیر، لغو کنند و تصمیم بگیرند که آیا یک به‌روزرسانی از راه دور را فعال کنند یا خیر. این متد قبل از انجام هرگونه بارگذاری اجرا می‌شود، بنابراین می‌توانید قبل از فعال کردن هرگونه بارگذاری محلی یا از راه دور، پایگاه داده را دستکاری کنید (برای مثال، داده‌های قدیمی را پاک کنید).

از آنجا که initialize() یک تابع غیرهمزمان است، می‌توانید داده‌ها را بارگذاری کنید تا ارتباط داده‌های موجود در پایگاه داده را تعیین کنید. رایج‌ترین حالت این است که داده‌های ذخیره شده فقط برای مدت زمان مشخصی معتبر باشند. RemoteMediator می‌تواند بررسی کند که آیا این زمان انقضا گذشته است یا خیر، که در این صورت کتابخانه Paging باید داده‌ها را به طور کامل به‌روزرسانی کند. پیاده‌سازی‌های initialize() باید یک InitializeAction به شرح زیر برگردانند:

  • در مواردی که داده‌های محلی نیاز به به‌روزرسانی کامل دارند، initialize() باید InitializeAction.LAUNCH_INITIAL_REFRESH را برگرداند. این باعث می‌شود RemoteMediator یک به‌روزرسانی از راه دور برای بارگذاری کامل داده‌ها انجام دهد. هرگونه بارگذاری APPEND یا PREPEND از راه دور، قبل از ادامه، منتظر موفقیت‌آمیز بودن بارگذاری REFRESH خواهد بود.
  • در مواردی که داده‌های محلی نیازی به به‌روزرسانی ندارند، initialize() باید InitializeAction.SKIP_INITIAL_REFRESH را برگرداند. این باعث می‌شود RemoteMediator از به‌روزرسانی از راه دور صرف‌نظر کرده و داده‌های ذخیره‌شده را بارگذاری کند.

جاوا

@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;
      }
    });
}

جاوا

@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);
}

ایجاد یک پیجر

در نهایت، شما باید یک نمونه Pager ایجاد کنید تا جریان داده‌های صفحه‌بندی شده را تنظیم کنید. این شبیه به ایجاد Pager از یک منبع داده شبکه ساده است، اما دو کار وجود دارد که باید متفاوت انجام دهید:

  • به جای ارسال مستقیم سازنده‌ی PagingSource ، باید متد query را ارائه دهید که یک شیء PagingSource را از DAO برمی‌گرداند.
  • شما باید یک نمونه از پیاده‌سازی RemoteMediator خود را به عنوان پارامتر remoteMediator ارائه دهید.

جاوا

UserDao userDao = database.userDao();
Pager<Integer, User> pager = Pager(
  new PagingConfig(/* pageSize = */ 20),
  null, // initialKey,
  new ExampleRemoteMediator(query, database, networkService)
  () -> userDao.pagingSource(query));

جاوا

UserDao userDao = database.userDao();
Pager<Integer, User> pager = Pager(
  new PagingConfig(/* pageSize = */ 20),
  null, // initialKey
  new ExampleRemoteMediator(query, database, networkService, bgExecutor),
  () -> userDao.pagingSource(query));

مدیریت کلیدهای از راه دور

کلیدهای راه دور ، کلیدهایی هستند که پیاده‌سازی RemoteMediator از آنها برای اعلام به سرویس backend که کدام داده‌ها را در مرحله بعد بارگذاری کند، استفاده می‌کند. در ساده‌ترین حالت، هر آیتم از داده‌های صفحه‌بندی شده شامل یک کلید راه دور است که می‌توانید به راحتی به آن ارجاع دهید. با این حال، اگر کلیدهای راه دور مربوط به آیتم‌های جداگانه نباشند، باید آنها را جداگانه ذخیره کرده و در متد load() خود مدیریت کنید.

این بخش نحوه جمع‌آوری، ذخیره و به‌روزرسانی کلیدهای راه دور که در اقلام جداگانه ذخیره نشده‌اند را شرح می‌دهد.

کلیدهای مورد

این بخش نحوه کار با کلیدهای راه دور مربوط به اقلام منفرد را شرح می‌دهد. معمولاً وقتی یک API اقلام منفرد را کلیدگذاری می‌کند، شناسه کالا به عنوان یک پارامتر پرس‌وجو ارسال می‌شود. نام پارامتر نشان می‌دهد که آیا سرور باید با اقلام قبل یا بعد از شناسه ارائه شده پاسخ دهد. در مثال کلاس مدل User ، فیلد id از سرور به عنوان یک کلید راه دور هنگام درخواست داده‌های اضافی استفاده می‌شود.

وقتی متد load() شما نیاز به مدیریت کلیدهای ریموت مخصوص هر آیتم دارد، این کلیدها معمولاً شناسه‌های داده‌های واکشی شده از سرور هستند. عملیات Refresh نیازی به کلید بارگذاری ندارند، زیرا آنها فقط جدیدترین داده‌ها را بازیابی می‌کنند. به طور مشابه، عملیات prepend نیازی به واکشی هیچ داده اضافی ندارند زیرا refresh همیشه جدیدترین داده‌ها را از سرور واکشی می‌کند.

با این حال، عملیات افزودن (append) به یک شناسه (ID) نیاز دارند. این کار مستلزم آن است که شما آخرین مورد را از پایگاه داده بارگذاری کنید و از شناسه آن برای بارگذاری صفحه بعدی داده‌ها استفاده کنید. اگر هیچ موردی در پایگاه داده وجود نداشته باشد، endOfPaginationReached روی true تنظیم می‌شود که نشان می‌دهد داده‌ها نیاز به به‌روزرسانی دارند.

جاوا

@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);
    });
}

جاوا

@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);
}

کلیدهای صفحه

این بخش نحوه کار با کلیدهای راه دور که با اقلام جداگانه مطابقت ندارند را شرح می‌دهد.

جدول کلید از راه دور را اضافه کنید

وقتی کلیدهای ریموت مستقیماً با آیتم‌های لیست مرتبط نیستند، بهتر است آنها را در یک جدول جداگانه در پایگاه داده محلی ذخیره کنید. یک موجودیت Room تعریف کنید که جدولی از کلیدهای ریموت را نشان می‌دهد:

جاوا

@Entity(tableName = "remote_keys")
public class RemoteKey {
  public String label;
  public String nextKey;
}

جاوا

@Entity(tableName = "remote_keys")
public class RemoteKey {
  public String label;
  public String nextKey;
}

شما همچنین باید یک DAO برای موجودیت RemoteKey تعریف کنید:

جاوا

@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);
}

جاوا

@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);
}

بارگیری با کلیدهای از راه دور

وقتی متد load() شما نیاز به مدیریت کلیدهای صفحه از راه دور دارد، باید آن را به روش‌های متفاوتی در مقایسه با استفاده اولیه از RemoteMediator تعریف کنید:

  • یک ویژگی اضافی که ارجاعی به DAO برای جدول کلید راه دور شما دارد، اضافه کنید.
  • با پرس و جو از جدول کلید راه دور به جای استفاده از PagingState ، مشخص کنید که کدام کلید در مرحله بعد بارگذاری شود.
  • کلید راه دور برگردانده شده از منبع داده شبکه را علاوه بر خود داده‌های صفحه‌بندی شده، وارد یا ذخیره کنید.

جاوا

@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);
    });
}

جاوا

@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);
}

منابع اضافی

برای کسب اطلاعات بیشتر در مورد کتابخانه Paging، به منابع اضافی زیر مراجعه کنید:

کدلبز

نمونه‌ها

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}