0Pricing
Android Academy · درس

PagingSource وPager

حدّد كيفية تحميل الصفحات

PagingSource وPager درس مجاني في Android Academy على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Android Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Android Academy 4 دروس في المجموع.

تحديد كيفية تحميل الصفحات

لتقسيم البيانات إلى صفحات من network API، تكتبون PagingSource. وهو يجيب عن سؤالين لصالح Paging 3:

  • كيف أحمّل الصفحة الخاصة بمفتاح معين؟
  • إذا حدّث المستخدم البيانات، فمن أين ينبغي أن أتابع؟

ثم تغلفونه داخل Pager لتحويله إلى Flow<PagingData>.

معاملات نوع PagingSource

يأخذ PagingSource<Key, Value> معاملَي نوع:

  • Key - يحدد صفحةً ما. في واجهة API تعتمد أرقام الصفحات يكون Int، أما في واجهة API تعتمد المؤشرات فيكون رمزًا مميزًا من النوع String.
  • Value - نوع العنصر، مثل Article.
import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource(
    private val api: ArticleApi
) : PagingSource<Int, Article>() {
    // implement load() and getRefreshKey()
}

تنفيذ load()

إن load() دالة suspend. تستقبل params.key، أي الصفحة المطلوب جلبها، وتعيد LoadResult.

عند النجاح، تعيدون LoadResult.Page مع العناصر ومفتاحَي الصفحة السابقة والتالية. ويعني استخدام null لأحد المفتاحين عدم وجود صفحة في ذلك الاتجاه.

override suspend fun load(
    params: LoadParams<Int>
): LoadResult<Int, Article> {
    val page = params.key ?: 1   // first load has a null key
    return try {
        val response = api.getArticles(page = page, size = params.loadSize)
        LoadResult.Page(
            data = response.items,
            prevKey = if (page == 1) null else page - 1,
            nextKey = if (response.items.isEmpty()) null else page + 1
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

أهمية prevKey وnextKey

تستخدم Paging قيمة nextKey للتحميل إلى الأمام أثناء تمرير المستخدم إلى الأسفل، وقيمة prevKey للتحميل إلى الخلف، وهو أمر مفيد عند البدء من منتصف القائمة.

يخبر إرجاع null في nextKey بأن الصفحات انتهت، فتتوقف Paging عن إرسال الطلبات. وقد يؤدي نسيان ذلك إلى طلبات فارغة لا نهائية.

// Stop forward paging when the server returns an empty page
nextKey = if (response.items.isEmpty()) null else page + 1

// Stop backward paging at the first page
prevKey = if (page == 1) null else page - 1

تنفيذ getRefreshKey()

عند تحديث القائمة، سواء بالسحب للتحديث أو بسبب إبطالها، تحتاج Paging إلى معرفة الصفحة التي ينبغي إعادة تحميلها حتى يبقى المستخدم تقريبًا في موضعه.

تستخدم getRefreshKey() قيمة anchorPosition الحالية، أي العنصر الأقرب إلى إطار العرض، لاختيار مفتاح مناسب.

override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
    return state.anchorPosition?.let { anchor ->
        val closestPage = state.closestPageToPosition(anchor)
        closestPage?.prevKey?.plus(1)
            ?: closestPage?.nextKey?.minus(1)
    }
}

معالجة الأخطاء بطريقة سليمة

ضعوا استدعاء الشبكة داخل try/catch، وأعيدوا LoadResult.Error(e) عند الفشل. تعرض Paging ذلك في واجهة المستخدم على هيئة LoadState.Error، مما يتيح لكم إظهار زر لإعادة المحاولة.

لا تسمحوا أبدًا بخروج استثناء من load()؛ بل التقطوه وحولوه إلى LoadResult.Error.

return try {
    val response = api.getArticles(page = page, size = params.loadSize)
    LoadResult.Page(
        data = response.items,
        prevKey = if (page == 1) null else page - 1,
        nextKey = if (response.items.isEmpty()) null else page + 1
    )
} catch (e: IOException) {        // no network
    LoadResult.Error(e)
} catch (e: HttpException) {       // non-2xx response
    LoadResult.Error(e)
}

إنشاء Pager

يربط Pager بين PagingConfig ومصنع ينشئ PagingSource جديدًا. وتكون الخاصية .flow فيه عبارة عن Flow<PagingData> تجمعها واجهة المستخدم.

يجب أن تنشئ lambda الخاصة بالمصنع مصدرًا جديدًا في كل مرة، لأن Paging تبطل المصدر وتعيد إنشاءه عند التحديث.

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

class ArticleRepository(private val api: ArticleApi) {
    fun articleStream(): Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = 20, prefetchDistance = 5),
        pagingSourceFactory = { ArticlePagingSource(api) }
    ).flow
}

cachedIn لـ ViewModels

يُعد جمع PagingData عملية تُنفذ مرة واحدة؛ إذ يؤدي جمعه مجددًا إلى بدء التحميل من جديد. وللاستمرار عبر تغييرات الإعدادات والسماح لعدة جامعين بمشاركة البيانات، خزّنوا التدفق مؤقتًا في viewModelScope باستخدام cachedIn.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn

class ArticleViewModel(
    repo: ArticleRepository
) : ViewModel() {
    val articles = repo.articleStream()
        .cachedIn(viewModelScope)
}

loadSize مقارنةً بـ pageSize

لاحظوا أن load() يقرأ params.loadSize، وليس pageSize الذي ضبطتموه مباشرةً.

قد تطلب Paging في التحميل الأولي جزءًا أكبر من البيانات، ويتحكم في ذلك initialLoadSize في PagingConfig، وتبلغ قيمته الافتراضية ثلاثة أضعاف حجم الصفحة. مرروا دائمًا params.loadSize إلى واجهة API الخاصة بكم حتى يتطابق الطلب مع الحجم الذي تتوقعه Paging.

// Correct: respect the size Paging asks for
val response = api.getArticles(page = page, size = params.loadSize)

// PagingConfig can tune the first load:
PagingConfig(pageSize = 20, initialLoadSize = 40)

واجهات API المعتمدة على المؤشرات

لا تستخدم كل واجهات API أرقام الصفحات. فبعضها يعيد مؤشرًا أو رمزًا مميزًا يشير إلى الصفحة التالية. والنمط مماثل تمامًا؛ ما عليكم سوى تغيير نوع Key إلى String واستخدام الرمز المميز الوارد في الاستجابة.

class CursorArticleSource(
    private val api: ArticleApi
) : PagingSource<String, Article>() {
    override suspend fun load(
        params: LoadParams<String>
    ): LoadResult<String, Article> {
        val cursor = params.key   // null on first load
        return try {
            val res = api.getArticles(cursor = cursor, size = params.loadSize)
            LoadResult.Page(
                data = res.items,
                prevKey = null,            // forward-only cursor
                nextKey = res.nextCursor   // null when exhausted
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<String, Article>) = null
}

جمع الأجزاء معًا

أصبح لديكم الآن طبقة البيانات كاملة: PagingSource يحمّل صفحةً واحدة، وPager يبث الصفحات، وViewModel يخزن التدفق مؤقتًا.

تجمع طبقة واجهة المستخدم ببساطة viewModel.articles، وسنعرضها في الدرس التالي باستخدام LazyColumn من Compose.

// Data layer summary
// 1. ArticlePagingSource : PagingSource<Int, Article>
// 2. Pager(config, factory).flow  -> Flow<PagingData<Article>>
// 3. ViewModel: repo.articleStream().cachedIn(viewModelScope)
// UI just collects viewModel.articles

تحقق سريع

في PagingSource يعتمد على أرقام الصفحات، إلى ماذا يشير إرجاع nextKey = null من load()؟

مراجعة: PagingSource وPager

أنشأتم طبقة البيانات الخاصة بتقسيم الصفحات:

  • ينفذ PagingSource<Key, Value> الدالتين load() وgetRefreshKey()
  • تعيد load() القيمة LoadResult.Page مع prevKey/nextKey، أو تعيد LoadResult.Error
  • يوقف المفتاح null تقسيم الصفحات في ذلك الاتجاه
  • ينتج Pager(config, factory).flow قيمة من النوع Flow<PagingData>
  • يحافظ cachedIn(viewModelScope) على البيانات عبر تغييرات الإعدادات

التالي: عرض هذا التدفق في قائمة Compose.

الأسئلة الشائعة

هل درس «PagingSource وPager» مجاني؟

نعم — نص درس «PagingSource وPager» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Android Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Android Academy 4 دروس في المجموع.

ماذا ستتعلم في «PagingSource وPager»؟

حدّد كيفية تحميل الصفحات تتمرن على Android Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Android Academy؟

لا تُشترط خبرة سابقة. Android Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.

كم من الوقت يستغرق درس «PagingSource وPager»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Android Academy هذا؟

نعم. كل درس في Android Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. لماذا نستخدم Paging
  2. PagingSource وPager
  3. Paging في قوائم Compose
  4. RemoteMediator والتخزين المؤقت
← العودة إلى Android Academy