تحميل البيانات المقسّمة على صفحات وعرضها

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

تحديد مصدر بيانات

الخطوة الأولى هي تحديد عملية تنفيذ PagingSource لتحديد مصدر البيانات. تتضمّن فئة واجهة برمجة التطبيقات PagingSource الطريقة load التي يمكنك إلغاؤها لتحديد كيفية استرداد البيانات المقسّمة إلى صفحات من مصدر البيانات المقابل.

استخدِم فئة PagingSource مباشرةً لاستخدام أنماط "كوروتين" في Kotlin للتحميل غير المتزامن.

اختيار أنواع المفاتيح والقيم

يحتوي PagingSource<Key, Value> على مَعلمتَي نوع: Key وValue. يحدّد المفتاح المعرّف المستخدَم لتحميل البيانات، والقيمة هي نوع البيانات نفسها. على سبيل المثال، إذا كنت تحمّل صفحات من عناصر User من الشبكة من خلال تمرير أرقام صفحات Int إلى Retrofit، اختَر Int كنوع Key وUser كنوع Value.

تحديد PagingSource

ينفّذ المثال التالي PagingSource الذي يحمّل صفحات العناصر حسب رقم الصفحة. نوع "Key" هو Int ونوع "Value" هو User.

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {

    init {
        // the data source is expected to be immutable
        // invalidate PagingSource if data source
        // has updated
        backEnd.addDatabaseOnChangedListener {
            invalidate()
        }
    }

    try {
      // Start refresh at page 1 if undefined.
      val nextPageNumber = params.key ?: 1
      val response = backend.searchUsers(query, nextPageNumber)
      return LoadResult.Page(
        data = response.users,
        prevKey = null, // Only paging forward.
        nextKey = nextPageNumber + 1
      )
    } catch (e: Exception) {
      // Handle errors in this block and return LoadResult.Error for
      // expected errors (such as a network failure).
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

يمرّر تنفيذ PagingSource المعتاد المَعلمات المقدَّمة في الدالة الإنشائية إلى طريقة load لتحميل البيانات المناسبة لطلب البحث. في المثال أعلاه، هذه المعلمات هي:

  • ‫backend: مثيل لخدمة الخلفية التي توفّر البيانات
  • ‫query: طلب البحث الذي سيتم إرساله إلى الخدمة المحدّدة من خلال backend

يحتوي العنصر LoadParams على معلومات عن عملية التحميل التي سيتم تنفيذها. يتضمن ذلك المفتاح الذي سيتم تحميله وعدد العناصر التي سيتم تحميلها. للتمييز بين أنواع التحميل في load، تحقَّق من الفئة الفرعية LoadParams التي تم تمريرها: LoadParams.Refresh أو LoadParams.Append أو LoadParams.Prepend.

يحتوي الكائن LoadResult على نتيجة عملية التحميل. ‫LoadResult هي فئة مغلقة تتّخذ أحد الأشكال الثلاثة التالية، استنادًا إلى ما إذا كان طلب load قد نجح:

  • إذا تم التحميل بنجاح، يتم عرض عنصر LoadResult.Page. مرِّر null لـ nextKey أو prevKey إذا لم يكن بالإمكان تحميل المزيد من البيانات في هذا الاتجاه.
  • إذا لم تنجح عملية التحميل، يتم عرض عنصر LoadResult.Error.
  • إذا لم يعُد PagingSource صالحًا بسبب تغيير البيانات الأساسية، يجب عرض عنصر LoadResult.Invalid.

يوضّح الشكل التالي كيفية تلقّي دالة load في هذا المثال للمفتاح لكل عملية تحميل وتوفير المفتاح لعملية التحميل اللاحقة.

في كل طلب تحميل، يأخذ ExamplePagingSource المفتاح الحالي
    ويعرض المفتاح التالي لتحميله.
الشكل 1: رسم بياني يوضّح كيفية استخدام load للمفتاح وتحديثه

يجب أن يتضمّن تنفيذ PagingSource أيضًا طريقة getRefreshKey التي تستخدم عنصر PagingState كمَعلمة. وتعرض المفتاح الذي سيتم تمريره إلى طريقة load عند إعادة تحميل البيانات أو إبطالها بعد التحميل الأولي. تستدعي مكتبة Paging هذه الطريقة تلقائيًا عند إعادة تحميل البيانات لاحقًا. بالنسبة إلى التقسيم إلى صفحات المستند إلى الإزاحة حيث تمثّل المفاتيح مواضع العناصر، يمكنك احتساب مفتاح إعادة التحميل بالنسبة إلى state.anchorPosition:

// Only for positional or offset PagingSources where Key is a 0-based item index
// rather than a page number.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
  // Center the refreshed load around the user's current viewport so the list
  // does not jump back to the top on invalidation.
  return state.anchorPosition?.let { anchorPosition ->
    (anchorPosition - state.config.initialLoadSize / 2).coerceAtLeast(0)
  }
}

معالجة الأخطاء

يمكن أن تفشل طلبات تحميل البيانات لعدد من الأسباب، خاصةً عند التحميل عبر شبكة. يمكنك الإبلاغ عن الأخطاء التي حدثت أثناء التحميل من خلال عرض عنصر LoadResult.Error من طريقة load.

على سبيل المثال، يمكنك رصد أخطاء التحميل والإبلاغ عنها في ExamplePagingSource من المثال السابق من خلال إضافة ما يلي إلى طريقة load:

catch (e: IOException) {
  // IOException for network failures.
  return LoadResult.Error(e)
} catch (e: HttpException) {
  // HttpException for any non-2xx HTTP status codes.
  return LoadResult.Error(e)
}

لمزيد من المعلومات حول معالجة أخطاء Retrofit، يُرجى الاطّلاع على النماذج في مرجع واجهة برمجة التطبيقات PagingSource.

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

إعداد مصدر بيانات PagingData

بعد ذلك، تحتاج إلى مصدر بيانات مقسَّمة إلى صفحات من تنفيذ PagingSource. أعِدّ مصدر البيانات في ViewModel. توفّر الفئة Pager طرقًا تعرض بثًا تفاعليًا لعناصر PagingData من PagingSource. تعرض مكتبة Paging تدفق البيانات على شكل Flow.

عند إنشاء مثيل Pager لإعداد البث التفاعلي، يجب تزويد المثيل بكائن ضبط PagingConfig ودالة تخبر Pager بكيفية الحصول على مثيل لتنفيذ PagingSource. يمكنك أيضًا تحديد مَعلمة initialKey اختيارية، مثل 1 لأرقام الصفحات التي تبدأ من 1 أو 0 للتنقّل بين الصفحات استنادًا إلى الإزاحة. في PagingConfig، يمكنك ضبط pageSize وinitialLoadSize وprefetchDistance وenablePlaceholders. تحدّد السمة prefetchDistance عدد العناصر من أي من حواف المحتوى الذي تم تحميله والتي تؤدي إلى جلب المحتوى التالي.

class UserViewModel(
    private val backend: ExampleBackendService,
    private val query: String
) : ViewModel() {

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        config = PagingConfig(
            pageSize = 20,
            // Load 3 pages initially so the user can scroll immediately without
            // hitting a loading state.
            initialLoadSize = 60,
            // Fetch the next page far enough ahead of the scroll position to
            // avoid visible pauses.
            prefetchDistance = 20,
            enablePlaceholders = true
        ),
        initialKey = 1,
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

يتيح عامل التشغيل cachedIn إمكانية مشاركة مصدر البيانات ويخزّن البيانات المحمَّلة مؤقتًا باستخدام CoroutineScope المقدَّم. بدون cachedIn، لا يمكن استرداد PagingData. يستخدم هذا المثال viewModelScope الذي يوفّره عنصر دورة الحياة lifecycle-viewmodel-ktx.

يستدعي العنصر Pager الإجراء load من العنصر PagingSource، ويزوّده بالعنصر LoadParams ويتلقّى العنصر LoadResult في المقابل.

جمع البيانات وعرضها في واجهة المستخدم

لربط مصدر البيانات المقسّم إلى صفحات بواجهة المستخدم، يمكنك الحصول على التدفق من ViewModel وتمريره إلى الدالة المركّبة للقائمة.

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userFlow = viewModel.userPagingFlow
    UserList(flow = userFlow)
}

استخدِم collectAsLazyPagingItems لتحويل مسار PagingData إلى LazyPagingItems. بعد ذلك، استخدِم واجهة برمجة التطبيقات items ضمن LazyColumn لتنسيق كل عنصر.

احرص على تقديم معرّف فريد وثابت لكل سلعة باستخدام السمة itemKey. يستخدم المثال التالي it.id (يشير إلى السمة User.id) لأنّه يظل ثابتًا لمثيل User في جميع تحديثات البيانات.

@Composable
fun UserList(flow: Flow<PagingData<User>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val user = lazyPagingItems[index]
            if (user != null) {
                UserRow(user)
            } else {
                UserPlaceholder()
            }
        }
    }
}

تستخدم مكتبة Paging القيمة null للعناصر النائبة أثناء تحميل الصفحة، لذا إذا فعّلت العناصر النائبة، عليك معالجة قيم null في مجموعة المحتوى.

تعرض القائمة الآن البيانات المقسّمة إلى صفحات، وتحمّل مكتبة Paging صفحات إضافية أثناء انتقال المستخدم للأسفل.

مراجع إضافية

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

الوثائق

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