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

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

این راهنما فرض می کند که شما با کتابخانه ماندگاری اتاق و استفاده اولیه از کتابخانه Paging آشنا هستید.

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

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

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

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

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

چرخه عمر صفحه بندی

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

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

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

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

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

استفاده اساسی

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

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

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

موجودیت‌های اتاق را ایجاد کنید

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

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

کاتلین

@Entity(tableName = "users")
data class User(val id: String, val label: String)

جاوا

@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) برای این موجودیت اتاق تعریف کنید، همانطور که در دسترسی به داده با استفاده از اتاق DAO توضیح داده شده است. DAO برای موجودیت آیتم لیست باید شامل روش های زیر باشد:

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

کاتلین

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

جاوا

@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 : پایگاه داده اتاق که به عنوان یک کش محلی عمل می کند.
  • networkService : یک نمونه API برای سرویس Backend.

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

کاتلین

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

جاوا

@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 را برگردانید.

کاتلین

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

جاوا

@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 برگرداند.LAUNCH_INITIAL_REFRESH. این باعث می شود که RemoteMediator یک به روز رسانی از راه دور را برای بارگیری کامل داده ها انجام دهد. هر بار APPEND یا PREPEND از راه دور قبل از ادامه، منتظر می ماند تا بارگیری REFRESH با موفقیت انجام شود.
  • در مواردی که داده‌های محلی نیازی به رفرش ندارند، initialize() باید InitializeAction.SKIP_INITIAL_REFRESH برگرداند.SKIP_INITIAL_REFRESH. این باعث می شود RemoteMediator از رفرش از راه دور صرف نظر کند و داده های کش را بارگیری کند.

کاتلین

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
  }
}

جاوا

@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 ارائه دهید.

کاتلین

val userDao = database.userDao()
val pager = Pager(
  config = PagingConfig(pageSize = 50)
  remoteMediator = 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)
  () -> 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));

شرایط مسابقه را مدیریت کنید

یکی از موقعیت‌هایی که برنامه شما باید هنگام بارگیری داده‌ها از چندین منبع مدیریت کند، این است که داده‌های حافظه پنهان محلی با منبع داده از راه دور همگام نمی‌شوند.

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

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

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

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

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

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

کلیدهای مورد

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

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

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

کاتلین

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

جاوا

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

کلیدهای صفحه

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

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

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

کاتلین

@Entity(tableName = "remote_keys")
data class RemoteKey(val label: String, val nextKey: String?)

جاوا

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

جاوا

@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 ، تعیین کنید کدام کلید بارگذاری شود.
  • کلید راه دور برگشتی را از منبع داده شبکه، علاوه بر خود داده های صفحه، درج یا ذخیره کنید.

کاتلین

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

جاوا

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

در جای خود تازه کنید

اگر برنامه شما مانند مثال‌های قبلی فقط نیاز به پشتیبانی از رفرش‌های شبکه از بالای فهرست دارد، RemoteMediator شما نیازی به تعریف رفتار بارگذاری پیش‌فرض ندارد.

با این حال، اگر برنامه شما نیاز به پشتیبانی از بارگیری تدریجی از شبکه در پایگاه داده محلی دارد، باید برای از سرگیری صفحه بندی با شروع از لنگر، موقعیت اسکرول کاربر، پشتیبانی ارائه دهید. پیاده‌سازی PagingSource اتاق این کار را برای شما انجام می‌دهد، اما اگر از Room استفاده نمی‌کنید، می‌توانید این کار را با لغو PagingSource.getRefreshKey() انجام دهید. برای اجرای نمونه getRefreshKey() به تعریف منبع Paging مراجعه کنید.

شکل 4 روند بارگیری داده ها را ابتدا از پایگاه داده محلی و سپس از شبکه به محض اینکه پایگاه داده از داده خارج شد را نشان می دهد.

PagingSource از پایگاه داده در UI بارگیری می شود تا زمانی که پایگاه داده از داده خارج شود. سپس RemoteMediator از شبکه در پایگاه داده بارگیری می شود و پس از آن PagingSource بارگذاری را ادامه می دهد.
شکل 4. نمودار نشان می دهد که چگونه PagingSource و RemoteMediator برای بارگذاری داده ها با هم کار می کنند.

منابع اضافی

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

Codelabs

نمونه ها

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}