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

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

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

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

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

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

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

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

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

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

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

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

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

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

شما همچنین باید یک شیء دسترسی به داده (DAO) برای این موجودیت Room همانطور که در بخش «دسترسی به داده‌ها با استفاده از DAOهای Room» توضیح داده شده است، تعریف کنید. 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()
}

پیاده‌سازی یک RemoteMediator

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

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

  • query : یک رشته‌ی پرس‌وجو که مشخص می‌کند کدام داده‌ها از سرویس backend بازیابی شوند.
  • database : پایگاه داده Room که به عنوان یک حافظه پنهان محلی عمل می‌کند.
  • 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 {
    // ...
  }
}

متد 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)
  }
}

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

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

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

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

ایجاد یک پیجر

در نهایت، شما باید یک نمونه 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)
}

رسیدگی به شرایط مسابقه

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

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

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

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

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

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

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

کلیدهای مورد

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

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

با این حال، عملیات افزودن (append) به یک شناسه (ID) نیاز دارند. این کار مستلزم آن است که شما آخرین مورد را از پایگاه داده بارگذاری کنید و از شناسه آن برای بارگذاری صفحه بعدی داده‌ها استفاده کنید. اگر هیچ موردی در پایگاه داده وجود نداشته باشد، 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)
    }
  }
}

کلیدهای صفحه

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

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

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

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

شما همچنین باید یک 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)
}

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

وقتی متد 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)
    }
  }
}

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

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

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

شکل ۲ فرآیند بارگذاری داده‌ها را ابتدا از پایگاه داده محلی و سپس از شبکه پس از اتمام داده‌ها در پایگاه داده نشان می‌دهد.

PagingSource از پایگاه داده به رابط کاربری بارگذاری می‌شود تا زمانی که پایگاه داده خالی از داده شود. سپس RemoteMediator از شبکه به پایگاه داده بارگذاری می‌شود و پس از آن PagingSource به بارگذاری ادامه می‌دهد.
شکل ۲. نموداری که نحوه‌ی همکاری PagingSource و RemoteMediator برای بارگذاری داده‌ها را نشان می‌دهد.

منابع اضافی

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

محتوا را مشاهده می‌کند

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