صفحة من الشبكة وقاعدة البيانات

يمكنك تقديم تجربة أفضل للمستخدمين من خلال التأكّد من إمكانية استخدام تطبيقك عندما تكون اتصالات الشبكة غير موثوقة أو عندما يكون المستخدم غير متصل بالإنترنت. ويمكنك إجراء ذلك من خلال تحميل البيانات من الشبكة ومن قاعدة بيانات محلية في الوقت نفسه. بهذه الطريقة، يعرض تطبيقك واجهة المستخدم من ذاكرة تخزين مؤقت لقاعدة بيانات محلية، ولا يرسل طلبات إلى الشبكة إلا عندما لا تتوفّر بيانات أخرى في قاعدة البيانات.

يفترض هذا الدليل أنّك على دراية بمكتبة Room لتخزين البيانات بشكل دائم والاستخدام الأساسي لمكتبة Paging.

تنسيق عمليات تحميل البيانات

توفّر مكتبة Paging المكوّن RemoteMediator لحالة الاستخدام هذه. تعمل RemoteMediator كإشارة من مكتبة Paging عندما ينتهي التطبيق من البيانات المخزّنة مؤقتًا. يمكنك استخدام هذه الإشارة لتحميل بيانات إضافية من الشبكة وتخزينها في قاعدة البيانات المحلية، حيث يمكن PagingSource تحميلها وتقديمها إلى واجهة المستخدم لعرضها.

عند الحاجة إلى بيانات إضافية، تستدعي مكتبة Paging الدالة load() من تنفيذ RemoteMediator. هذه دالة تعليق، لذا يمكن تنفيذ مهام طويلة الأمد بأمان. تجلب هذه الدالة عادةً البيانات الجديدة من مصدر على الشبكة وتحفظها في مساحة التخزين المحلية.

تعمل هذه العملية مع البيانات الجديدة، ولكن بمرور الوقت، تتطلب البيانات المخزَّنة في قاعدة البيانات إبطال الصلاحية، مثلاً عندما يطلب المستخدم إعادة التحميل يدويًا. يتم تمثيل ذلك باستخدام السمة LoadType التي يتم تمريرها إلى الطريقة load(). يُعلم LoadType RemoteMediator ما إذا كان بحاجة إلى إعادة تحميل البيانات الحالية أو جلب بيانات إضافية يجب إلحاقها أو إضافتها قبل القائمة الحالية.

بهذه الطريقة، يضمن RemoteMediator أن يحمّل تطبيقك البيانات التي يريد المستخدمون رؤيتها بالترتيب المناسب.

مراحل النشاط في التقسيم إلى صفحات

عند طلب تقسيم النتائج مباشرةً من الشبكة، يحمّل PagingSource البيانات ويعرض كائن LoadResult. يتم تمرير عملية تنفيذ PagingSource إلى Pager من خلال المعلّمة pagingSourceFactory.

بما أنّ واجهة المستخدم تتطلّب بيانات جديدة، يستدعي Pager الطريقة load() من PagingSource ويعرض مجموعة من عناصر PagingData التي تتضمّن البيانات الجديدة. يتم عادةً تخزين كل عنصر PagingData مؤقتًا في ViewModel قبل إرساله إلى واجهة المستخدم لعرضه.

الشكل 1. مخطّط لدورة حياة Paging مع PagingSource وRemoteMediator

تؤدي عملية RemoteMediator إلى تغيير تدفّق البيانات هذا. يؤدي PagingSource إلى تحميل البيانات، ولكن عند استنفاد البيانات المقسّمة إلى صفحات، تشغّل مكتبة Paging RemoteMediator لتحميل بيانات جديدة من مصدر الشبكة. يخزّن RemoteMediator البيانات الجديدة في قاعدة البيانات المحلية، لذا لا حاجة إلى ذاكرة تخزين مؤقت داخلية في ViewModel. أخيرًا، يبطل PagingSource صلاحيته، وينشئ Pager مثيلاً جديدًا لتحميل البيانات الجديدة من قاعدة البيانات.

الاستخدام الأساسي

لنفترض أنّك تريد أن يحمّل تطبيقك صفحات تتضمّن User عنصرًا من مصدر بيانات شبكة يستند إلى مفتاح عنصر إلى ذاكرة تخزين مؤقت محلية مخزَّنة في قاعدة بيانات Room.

تساعد عملية تنفيذ RemoteMediator في تحميل البيانات المقسّمة إلى صفحات من الشبكة إلى قاعدة البيانات، ولكنّها لا تحمّل البيانات مباشرةً إلى واجهة المستخدم. وبدلاً من ذلك، يستخدم التطبيق قاعدة البيانات كمصدر موثوق. بعبارة أخرى، يعرض التطبيق البيانات التي تم تخزينها مؤقتًا في قاعدة البيانات فقط. تتولّى عملية التنفيذ (على سبيل المثال، تلك التي تم إنشاؤها بواسطة Room) تحميل البيانات المخزّنة مؤقتًا من قاعدة البيانات إلى واجهة المستخدم.PagingSource

إنشاء عناصر Room

تتمثل الخطوة الأولى في استخدام مكتبة Room لتخزين البيانات بشكل دائم من أجل تحديد قاعدة بيانات تحتوي على ذاكرة تخزين مؤقت محلية للبيانات المقسّمة على صفحات من مصدر بيانات الشبكة. ابدأ بتنفيذ RoomDatabase كما هو موضّح في حفظ البيانات في قاعدة بيانات محلية باستخدام Room.

بعد ذلك، حدِّد كيان Room لتمثيل جدول بعناصر القائمة كما هو موضّح في تحديد البيانات باستخدام كيانات Room. امنحها حقل id كمفتاح أساسي، بالإضافة إلى حقول لأي معلومات أخرى تتضمّنها عناصر القائمة.

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

يجب أيضًا تحديد كائن الوصول إلى البيانات (DAO) الخاص بكيان Room هذا كما هو موضّح في الوصول إلى البيانات باستخدام كائنات الوصول إلى البيانات في 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: سلسلة طلب تحدّد البيانات التي سيتم استردادها من خدمة الخلفية.
  • 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). بعد تخزين البيانات، عليك إبطال مصدر البيانات لإعلام مكتبة 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)
  }
}

تحديد طريقة initialize

يمكن أن تتجاوز عمليات تنفيذ 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 مباشرةً، يجب توفير طريقة طلب البحث التي تعرض عنصر 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().

يوضّح هذا القسم كيفية جمع المفاتيح البعيدة وتخزينها وتعديلها، وهي المفاتيح التي لا يتم تخزينها في عناصر فردية.

مفاتيح العناصر

يوضّح هذا القسم كيفية استخدام المفاتيح البعيدة التي تتوافق مع عناصر فردية. عادةً، عندما يتم إيقاف مفاتيح واجهة برمجة التطبيقات لعناصر فردية، يتم تمرير رقم تعريف العنصر كمَعلمة طلب بحث. يشير اسم المَعلمة إلى ما إذا كان يجب أن يستجيب الخادم بعناصر قبل المعرّف المقدَّم أو بعده. في مثال فئة النموذج User، يتم استخدام الحقل id من الخادم كمفتاح بعيد عند طلب بيانات إضافية.

عندما تحتاج طريقتك load() إلى إدارة المفاتيح البعيدة الخاصة بالعناصر، تكون هذه المفاتيح عادةً هي معرّفات البيانات التي يتم جلبها من الخادم. لا تحتاج عمليات إعادة التحميل إلى مفتاح تحميل، لأنّها تسترد أحدث البيانات فقط. وبالمثل، لا تحتاج عمليات الإضافة في المقدّمة إلى جلب أي بيانات إضافية لأنّ عملية إعادة التحميل تجلب دائمًا أحدث البيانات من الخادم.

ومع ذلك، تتطلّب عمليات الإلحاق توفير معرّف. يتطلّب ذلك تحميل العنصر الأخير من قاعدة البيانات واستخدام رقم تعريفه لتحميل الصفحة التالية من البيانات. إذا لم تكن هناك أي عناصر في قاعدة البيانات، سيتم ضبط قيمة endOfPaginationReached على "صحيح"، ما يشير إلى ضرورة إعادة تحميل البيانات.

@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.

يوضّح الشكل 2 عملية تحميل البيانات أولاً من قاعدة البيانات المحلية، ثم من الشبكة عندما تنتهي البيانات في قاعدة البيانات.

تحمّل PagingSource البيانات من قاعدة البيانات إلى واجهة المستخدم إلى أن تنتهي البيانات في قاعدة البيانات. بعد ذلك، يتم تحميل RemoteMediator من الشبكة إلى قاعدة البيانات، ثم يواصل PagingSource عملية التحميل.
الشكل 2. مخطّط يوضّح كيفية عمل PagingSource وRemoteMediator معًا لتحميل البيانات

مراجع إضافية

لمزيد من المعلومات حول مكتبة Paging، يُرجى الاطّلاع على المراجع الإضافية التالية:

مشاهدة المحتوى