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

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

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

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

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

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

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

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

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

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

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

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

‫RemoteMediator این جریان داده را تغییر می‌دهد. PagingSource همچنان داده‌ها را بار می‌کند؛ اما وقتی داده‌های صفحه‌بندی‌شده تمام می‌شود، کتابخانه «صفحه‌بندی» 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 تعریف کنید، همان‌طور که در دسترسی به داده‌ها بااستفاده از Room DAOs شرح داده شده است. 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: رشته پُرسمانی که مشخص می‌کند کدام داده‌ها از سرویس پس‌نما بازیابی شود.
  • database: پایگاه داده Room که به‌عنوان حافظه نهان محلی عمل می‌کند.
  • networkService: نمونه میانای برنامه‌سازی کاربردی برای سرویس پس‌نهایی.

پیاده‌سازی 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) را برگردانید. پس‌از ذخیره شدن داده‌ها، منبع داده را باطل کنید تا کتابخانه «صفحه‌بندی» از داده‌های جدید مطلع شود.
    • اگر بارگیری موفقیت‌آمیز باشد و فهرست دریافتی موارد خالی باشد یا آخرین نمایه صفحه باشد، MediatorResult.Success(endOfPaginationReached = true) را برگردانید. پس‌از ذخیره شدن داده‌ها، منبع داده را نامعتبر کنید تا کتابخانه «صفحه‌بندی» از داده‌های جدید مطلع شود.
    • اگر درخواست باعث ایجاد خطا شد، 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 می‌تواند بررسی کند که آیا این زمان انقضا گذشته است یا نه، که در این صورت کتابخانه «صفحه‌بندی» باید داده‌ها را کاملاً بازآوری کند. پیاده‌سازی‌های 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، باید روش پُرسمانی را ارائه دهید که شیء 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 از آن‌ها استفاده می‌کند تا به سرویس پس‌بک بگوید کدام داده‌ها را در مرحله بعد بار کند. در ساده‌ترین حالت، هر مورد از داده‌های صفحه‌بندی‌شده شامل یک کلید ازراه‌دور است که به‌راحتی می‌توانید به آن ارجاع دهید. بااین‌حال، اگر کلیدهای راه دور با موارد جداگانه مطابقت نداشته باشند، باید آن‌ها را جداگانه ذخیره کنید و در روش load() خود مدیریت کنید.

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

کلیدهای مورد

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

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

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

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

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

‫PagingSource از پایگاه داده در میانای کاربر بار می‌شود تا زمانی که داده‌های پایگاه داده
    تمام شود. سپس RemoteMediator از شبکه در پایگاه داده بار می‌شود و پس‌از آن PagingSource به بار کردن ادامه می‌دهد.
شکل ۲. نموداری که نشان می‌دهد PagingSource و RemoteMediator چگونه باهم کار می‌کنند تا داده‌ها را بار کنند.

منابع بیشتر

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

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