آموزش Paging 3 در اندروید: صفحه بندی داده ها در کامپوز

برنامه نویسی اندروید
آموزش Paging 3 در اندروید: صفحه بندی داده ها در کامپوز

طراحی و مدیریت فهرست های بی پایان داده ها (Infinite Scrolling) در توسعه اپلیکیشن های موبایل، معیاری تعیین کننده برای سنجش کارایی رم، روانی اسکرول و بلوغ معماری یک محصول نرم افزاری به شمار می رود.

در گذشته، لود کردن همزمان صدها رکورد از دیتابیس یا وب سرویس، فضای Heap سیستم عامل را به سرعت پر می کرد و با ایجاد خطای فاجعه بار OutOfMemoryError، لکنت شدید اسکرول (Jank) و توقف های مکرر زباله روب (Garbage Collector)، تجربه کاربری را به کلی تخریب می نمود.

برای حل بنیادین این چالش، شرکت گوگل کتابخانه قدرتمند جت پک پیجینگ ۳ (Jetpack Paging 3) را به عنوان نسل نوین ابزار های صفحه بندی داده ها در اندروید عرضه کرد.

پیجینگ ۳ با بازنویسی کامل بر پایه کاتلین و کوروتین ها، جریان داده های صفحه بندی شده را به صورت قطعه های بهینه (Chunks) مدیریت می کند، ارتباط بومی با جت پک کامپوز را فراهم می سازد و با بهره گیری از معماری آفلاین فرست از طریق RemoteMediator و دیتابیس Room در اندروید، امکان اسکرول بی نهایت و روان را تضمین می کند.

در این راهنمای جامع و مرجع در حوزه برنامه نویسی اندروید، با کالبدشکافی معماری سه لایه Paging 3، کد نویسی گام به گام PagingSource، مدیریت وضعیت های بارگذاری در کامپوز، تلفیق با کتابخانه رتروفیت Retrofit و پیاده سازی پیشرفته کش محلی، صفر تا صد مهندسی صفحه بندی مدرن را فرا می گیرید.

نتیجه گیری مهندسی در یک نگاه (Executive Summary): کتابخانه Jetpack Paging ۳ استاندارد رسمی گوگل برای بارگذاری تدریجی داده های حجیم در اندروید است. پیجینگ ۳ با معماری سه لایه بر بستر کاتلین فلو، بارگذاری ناهمگام روی ترد پس زمینه، مصرف بهینه حافظه رم از طریق کش هوشمند و یکپارچگی بومی با LazyColumn در جت پک کامپوز، خطای کمبود حافظه (OOM) را به صفر رسانده و تجربه اسکرول روان ۶۰ و ۱۲۰ فریم بر ثانیه را تضمین می کند.

دستاوردهای کلیدی و آموزه های فنی این راهنما

  • حذف خطای OutOfMemory: مدیریت حافظه هوشمند با بارگذاری صرفاً داده های در حال مشاهده و بازیافت خودکار صفحات دور از دید کاربر.
  • جریان داده های واکنشی با Flow: هماهنگی کامل با استریم های کاتلین فلو و کش کردن داده ها در طول چرخه حیات ویومدل.
  • مدیریت جامع LoadState: تشخیص خودکار وضعیت های بارگذاری اولیه (Refresh)، ادامه لیست (Append) و خطاهای شبکه بدون کد نویسی دستی.
  • معماری آفلاین فرست با RemoteMediator: هماهنگی تراکنشی میان سرور ریموت و دیتابیس محلی Room به عنوان منبع واحد حقیقت (SSOT).
  • یکپارچگی بی نقص با Compose: بهره گیری از متد collectAsLazyPagingItems برای بازترکیب نرم و بدون لرزش کامپوننت LazyColumn.

۱. چالش های مدیریت داده های حجیم در لیست ها و چرایی شکست روش های سنتی

در اپلیکیشن های مدرن مقیاس بالا، فهرست ها صرفاً حاوی چند سطر متن ساده نیستند، بلکه شامل تصاویر باکیفیت، ویجت های تعاملی، ویدیوها و داده های پیچیده لایه شبکه می شوند.

روش سنتی بارگذاری لیست ها که در آن توسعه دهندگان کل آرایه داده ها را از طریق درخواست شبکه فراخوانی کرده و درون یک شیء List<Item> در حافظه رم نگه می داشتند، در مقیاس تولید با شکست های فاجعه باری مواجه می شد.

نخستین بحران، اشغال بی رویه فضای Heap سیستم عامل اندروید است. هنگامی که کاربر درون لیستی با چند صد آیتم اسکرول می کند، شیءهای انباشته شده باعث می شوند موتور اجرای اندروید (ART) به طور مداوم فرآیند Garbage Collection را فعال سازد که نتیجه آن توقف های چند میلی ثانیه ای فریم ها (Frame Drops) و لکنت شدید است.

معماری سه لایه کتابخانه Paging 3 در اندروید
نمای سه لایه معماری جت پک پیجینگ ۳: تفکیک دقیق وظایف میان لایه مخزن داده (Repository)، لایه ویومدل (ViewModel) و لایه نمایش در جت پک کامپوز

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

سومین چالش بزرگ، دشواری مدیریت حالت های مختلف نظیر قطع ناگهانی اینترنت در میانه اسکرول، افزودن دکمه تلاش مجدد (Retry) در انتهای لیست و جلوگیری از ارسال درخواست های تکراری (Duplicate Requests) حین اسکرول سریع بود.

کتابخانه Paging 3 تمامی این مشکلات زیرساختی را با یک الگوی مدرن حل کرده است و بارگذاری، کش و رندر را بر پایه اصول معماری تمیز جداسازی می کند.

۲. ماتریس مقایسه فنی: Paging ۲ در برابر نسخه مدرن Paging ۳

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

جدول زیر تحول انقلابی رخ داده در Paging 3 را به تصویر می کشد:

شاخص ارزیابی معماری ابزار قدیمی Paging ۲ کتابخانه مدرن Paging ۳
زبان و الگوی همزمانی متکی بر کالبک های جاوا، RxJava و LiveData ۱۰۰٪ کاتلین نیتیو با Coroutines و Flow
کلاس منبع داده سه کلاس مجزای Positional, ItemKeyed, PageKeyed یک کلاس یکپارچه و منعطف به نام PagingSource
پشتیبانی از Jetpack Compose عدم پشتیبانی نیتیو (نیاز به پل های دستی) پشتیبانی کامل و مستقیم با collectAsLazyPagingItems
مدیریت وضعیت بارگذاری و خطا نیاز به طراحی کلاس های پیچیده State Wrapper سیستم داخلی قدرتمند CombinedLoadStates
پشتیبانی از کش محلی و آفلاین کلاس پیچیده BoundaryCallback با خطاهای سینک کلاس پیشرفته و ترانزاکشنال RemoteMediator
قابلیت Retry و Refresh خودکار پیاده سازی دستی و آسیب پذیر توابع آماده و پیش ساخته retry() و refresh()
جداسازی فوتر و هدر لودینگ پیاده سازی چندگانه ViewType در آداپتر متدهای بومی withLoadStateHeaderAndFooter

همان گونه که در جدول مشاهده می شود، Paging 3 نه تنها کدهای تکراری را بیش از ۷۰ درصد کاهش داده، بلکه قابلیت اطمینان و سرعت اجرا را به حداکثر رسانده است.

۳. تشریح معماری سه لایه Paging ۳: مخزن داده، ویومدل و لایه نمایش

معماری Paging 3 بر تفکیک کامل لایه ها استوار است تا هیچ وابستگی مستقیمی میان نحوه دریافت داده از سرور و نحوه نمایش آن در پیکسل های صفحه وجود نداشته باشد:

  1. لایه مخزن داده (Repository Layer): هسته اصلی این لایه شیء PagingSource است که وظیفه دارد یک صفحه از داده ها را از یک منبع (مثلاً رتروفیت) بخواند. در سناریوهای آفلاین، کلاس RemoteMediator وظیفه هماهنگی دریافت داده های وب سرویس و درج آن ها در دیتابیس Room را به عهده می گیرد.
  2. لایه ویومدل (ViewModel Layer): در این لایه، کامپوننت Pager با دریافت تنظیمات ابعاد صفحه (PagingConfig)، یک شیء از نوع Flow<PagingData<T>> تولید می کند. متد حیاتی cachedIn(viewModelScope) این جریان را در چرخه حیات ویومدل کش می کند تا با چرخش گوشی یا تغییر وضعیت UI، داده ها مجدداً لود نشوند.
  3. لایه نمایش (UI Layer): در دنیای کامپوز، اکستنشن collectAsLazyPagingItems() استریم PagingData را به لیستی واکنشی برای LazyColumn یا LazyRow تبدیل می کند و وضعیت های لودینگ را به سادگی در اختیار کامپوننت های فرزند می گذارد.

۴. راه اندازی گریدل و افزودن وابستگی های Paging ۳ به پروژه

برای آغاز پیاده سازی، باید کتابخانه های رسمی Paging 3 و هماهنگ سازهای آن با کامپوز و روم را به فایل build.gradle.kts اضافه نمایید:

// build.gradle.kts (Module: app)
dependencies {
    val pagingVersion = "3.3.2"

    // وابستگی اصلی Paging 3
    implementation("androidx.paging:paging-runtime-ktx:$pagingVersion")

    // ماژول یکپارچه ساز Paging 3 با Jetpack Compose
    implementation("androidx.paging:paging-compose:$pagingVersion")

    // ماژول ادغام Paging 3 با دیتابیس Room (اختیاری برای حالت آفلاین)
    implementation("androidx.room:room-paging:2.6.1")

    // کتابخانه های شبکه و رتروفیت
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-gson:2.11.0")
}

توصیه می شود همواره از نسخه سازگار با پلتفرم Compose BOM استفاده کنید تا از بروز تداخل نسخه های کامپایلر کاتلین پیشگیری شود.

۵. پیاده سازی کلاس PagingSource: اتصال به وب سرویس با رتروفیت

کلاس PagingSource تعریف می کند که داده ها چگونه و بر اساس چه کلیدی (مثلاً شماره صفحه یا اشاره گر مکان نما) واکشی شوند.

ابتدا یک اینترفیس رتروفیت برای دریافت محصولات صفحه بندی شده تعریف می کنیم:

// تعریف اینترفیس وب سرویس رتروفیت
interface ProductApiService {
    @GET("api/v1/products")
    suspend fun getProducts(
        @Query("page") page: Int,
        @Query("size") pageSize: Int
    ): ProductApiResponse
}

data class ProductApiResponse(
    val data: List,
    val totalPages: Int,
    val currentPage: Int
)

data class ProductItem(
    val id: Long,
    val title: String,
    val price: Double,
    val imageUrl: String
)

اکنون متد load را در کلاس سفارشی ProductPagingSource برای واکشی ناهمگام صفحات پیاده سازی می نماییم:

// پیاده سازی تابع load در PagingSource
class ProductPagingSource(
    private val apiService: ProductApiService
) : PagingSource() {

    override suspend fun load(params: LoadParams): LoadResult {
        val position = params.key ?: 1
        return try {
            val response = apiService.getProducts(page = position, pageSize = params.loadSize)
            val products = response.data
            val nextKey = if (products.isEmpty() || position >= response.totalPages) null else position + 1

            LoadResult.Page(
                data = products,
                prevKey = if (position == 1) null else position - 1,
                nextKey = nextKey
            )
        } catch (exception: IOException) {
            LoadResult.Error(exception)
        } catch (exception: HttpException) {
            LoadResult.Error(exception)
        }
    }
}

دومین متد ضروری، متد getRefreshKey است که در زمان بارگذاری مجدد (Refresh) لیست اجرا می شود تا موقعیت اسکرول کاربر حفظ گردد:

// محاسبه کلید صفحه نزدیک برای بازیابی اسکرول در زمان ریفرش
override fun getRefreshKey(state: PagingState): Int? {
    return state.anchorPosition?.let { anchorPosition ->
        state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
            ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
    }
}

متد getRefreshKey نزدیک ترین صفحه معتبر به آخرین محل اسکرول کاربر (anchorPosition) را بازمی گرداند و مانع از پرش صفحه به اولین سطر می شود.

۶. لایه ویومدل: پیکربندی Pager و استریم Flow با متد cachedIn

در لایه ویومدل، با استفاده از کلاس Pager منبع داده را به یک استریم ناهمگام تبدیل می کنیم.

به لطف معماری مبتنی بر کروتین و فلو در کاتلین، کلیه پارامترهای رفتاری صفحه بندی از طریق شیء PagingConfig پیکربندی می شوند:

// پیکربندی شیء Pager در لایه ریپازیتوری
class ProductRepository(private val apiService: ProductApiService) {

    fun getProductStream(): Flow> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                prefetchDistance = 5,
                enablePlaceholders = false,
                initialLoadSize = 40,
                maxSize = 200
            ),
            pagingSourceFactory = { ProductPagingSource(apiService) }
        ).flow
    }
}

شناخت عمیق پارامترهای PagingConfig برای جلوگیری از باگ های عملکردی حیاتی است:

  • pageSize: تعداد رکوردهایی که در هر درخواست خوانده می شوند (معمولاً بین ۱۵ تا ۳۰ آیتم).
  • prefetchDistance: فاصله پیش بارگذاری؛ هنگامی که کاربر به ۵ آیتم مانده به انتهای لیست فعلی برسد، پیجینگ ۳ درخواست دریافت صفحه بعدی را در پس زمینه ارسال می کند تا کاربر هیچ توقفی حس نکند.
  • enablePlaceholders: در صورت فعال بودن (true)، برای آیتم هایی که هنوز لود نشده اند آیتم های خالی رندر می کند که برای اسکرول بار دقیق دیتابیس مناسب است؛ اما برای وب سرویس های با اندازه نامشخص باید false تنظیم گردد.
  • initialLoadSize: تعداد آیتم های اولین بارگذاری که معمولاً دو یا سه برابر pageSize در نظر گرفته می شود تا صفحه نمایش در نخستین ورود کاملاً پر شود.
  • maxSize: سقف مجاز آیتم های نگه داری شده در رم؛ با اسکرول کاربر به فواصل بسیار دور، صفحات اولیه به طور خودکار از حافظه دور ریخته می شوند تا از خطای OOM جلوگیری گردد.

اکنون در ProductViewModel، استریم فلو را با استفاده از متد cachedIn کش می کنیم:

// کش کردن استریم داده در ویومدل با متد cachedIn
class ProductViewModel(
    private val repository: ProductRepository
) : ViewModel() {

    val productListState: Flow> = repository.getProductStream()
        .cachedIn(viewModelScope)
}
قاعده حیاتی کش در ویومدل: اگر متد cachedIn(viewModelScope) را فراخوانی نکنید، با هر بار بازترکیب کامپوز یا چرخش صفحه، کل فرآیند صفحه بندی از ابتدا ریست شده و درخواست های شبکه غیرضروری به سرور ارسال می شوند.

۷. پیاده سازی در Jetpack Compose با LazyColumn و collectAsLazyPagingItems

جذاب ترین بخش کتابخانه Paging 3، هماهنگی خارق العاده آن با دنیای مدرن جت پک کامپوز است.

اکستنشن collectAsLazyPagingItems() استریم فلو را دریافت کرده و به یک لیست تطبیق پذیر تبدیل می کند:

مدیریت وضعیت بارگذاری Paging 3 در کامپوز
مدیریت جامع چرخه حیات LoadState در جت پک کامپوز: تفکیک حالت های بارگذاری اولیه (Refresh)، بارگذاری صفحات بعدی (Append) و مدیریت کارت خطای شبکه با دکمه تلاش مجدد

قطعه کد زیر پیاده سازی ساختار اصلی لیست در کامپوز با تعیین کلیدهای یکتا را نشان می دهد:

// رندر آیتم های صفحه بندی شده با LazyColumn در کامپوز
@Composable
fun ProductListContent(
    lazyPagingItems: LazyPagingItems,
    modifier: Modifier = Modifier
) {
    LazyColumn(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(16.dp)
    ) {
        items(
            count = lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val item = lazyPagingItems[index]
            if (item != null) {
                ProductCard(product = item)
            }
        }
        renderAppendState(lazyPagingItems)
    }
}

سپس وضعیت بارگذاری انتهای لیست (Append Loading Indicator) به عنوان یک آیتم مجزا به پایین لیست متصل می شود:

// نمایش اسپینر لودینگ در انتهای لیست هنگام اسکرول
fun LazyListScope.renderAppendState(lazyPagingItems: LazyPagingItems) {
    item {
        if (lazyPagingItems.loadState.append is LoadState.Loading) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp),
                contentAlignment = Alignment.Center
            ) {
                CircularProgressIndicator(strokeWidth = 3.dp)
            }
        }
    }
}

استفاده از lazyPagingItems.itemKey { it.id } یک الزام حیاتی در عملکرد است. این شناسه به کامپوز اجازه می دهد انیمیشن های جابجایی آیتم ها را بدون رندر مجدد کل سطرها انجام دهد.

۸. مدیریت خطاهای شبکه و وضعیت های بارگذاری (CombinedLoadStates)

کتابخانه Paging 3 به صورت پیش فرض تمامی وضعیت های بارگذاری را در قالبی به نام CombinedLoadStates گزارش می کند:

  • loadState.refresh: وضعیت اولین بارگذاری لیست یا ریفرش کامل با کشیدن انگشت به پایین (Swipe-to-Refresh).
  • loadState.append: وضعیت بارگذاری صفحات بعدی در انتهای لیست هنگام اسکرول به سمت پایین.
  • loadState.prepend: وضعیت بارگذاری صفحات قبلی در صورت اسکرول به سمت بالا.

هر یک از این وضعیت ها می تواند در یکی از سه حالت LoadState.Loading، LoadState.NotLoading یا LoadState.Error قرار گیرد.

کامپوننت زیر یک کارت حرفه ای برای نمایش خطای اینترنت به همراه دکمه تلاش مجدد (Retry) را پیاده سازی می کند:

// ویجت مدیریت خطای شبکه با دکمه تلاش مجدد در کامپوز
@Composable
fun ErrorRetryItem(
    message: String,
    onRetryClick: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
    ) {
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(
                text = message,
                color = MaterialTheme.colorScheme.onErrorContainer,
                style = MaterialTheme.typography.bodyMedium
            )
            Spacer(modifier = Modifier.height(8.dp))
            Button(onClick = onRetryClick) {
                Text("تلاش مجدد")
            }
        }
    }
}

همچنین برای پیاده سازی قابلیت کشیدن صفحه به پایین جهت نوسازی (Pull-to-Refresh)، کافی است متد lazyPagingItems.refresh() را در هندلر کامپوننت متصل نمایید:

// اتصال Swipe-to-Refresh به استریم Paging 3
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PagingPullRefreshBox(
    lazyPagingItems: LazyPagingItems
) {
    val isRefreshing = lazyPagingItems.loadState.refresh is LoadState.Loading
    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = { lazyPagingItems.refresh() }
    ) {
        ProductListContent(lazyPagingItems = lazyPagingItems)
    }
}

۹. معماری آفلاین فرست با RemoteMediator و دیتابیس Room

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

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

کلاس RemoteMediator پلی اتمیک میان وب سرویس و دیتابیس محلی است که دیتابیس Room را به عنوان منبع واحد حقیقت (Single Source of Truth) تثبیت می کند:

معماری آفلاین فرست Paging 3 با RemoteMediator
جریان داده در معماری آفلاین فرست: سرور داده ها را به RemoteMediator ارسال می کند؛ داده ها در Room کش می شوند و سپس PagingSource دیتابیس، رابط کاربری کامپوز را تغذیه می نماید

در این الگو، لایه UI هرگز مستقیماً با شبکه صحبت نمی کند؛ بلکه کامپوز همواره به PagingSource تولید شده توسط Room گوش می دهد و هرگاه کاربر به انتهای لیست دیتابیس رسید، RemoteMediator به صورت خودکار فعال شده و داده های جدید را از شبکه دریافت و در دیتابیس تزریق می کند.

۱۰. پیاده سازی عملیاتی RemoteMediator و جدول RemoteKeys

برای پیاده سازی RemoteMediator، نیازمند یک جدول دیتابیس جانبی به نام RemoteKeys هستیم تا شماره صفحه قبلی و بعدی هر آیتم را روی حافظه ذخیره نماید:

// موجودیت جدول کلیدهای صفحه بندی در دیتابیس Room
@Entity(tableName = "remote_keys")
data class RemoteKeys(
    @PrimaryKey val productId: Long,
    val prevKey: Int?,
    val nextKey: Int?
)

@Dao
interface RemoteKeysDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAll(remoteKeys: List)

    @Query("SELECT * FROM remote_keys WHERE productId = :productId")
    suspend fun getRemoteKeysByProductId(productId: Long): RemoteKeys?

    @Query("DELETE FROM remote_keys")
    suspend fun clearRemoteKeys()
}

اکنون متد load هماهنگ ساز آفلاین فرست را با تراکنش اتمیک دیتابیس پیاده سازی می کنیم:

// متد load در RemoteMediator برای هماهنگی شبکه و دیتابیس
@OptIn(ExperimentalPagingApi::class)
class ProductRemoteMediator(
    private val database: AppDatabase,
    private val apiService: ProductApiService
) : RemoteMediator() {

    override suspend fun load(loadType: LoadType, state: PagingState): MediatorResult {
        val page = when (loadType) {
            LoadType.REFRESH -> getRemoteKeyClosestToPosition(state)?.nextKey?.minus(1) ?: 1
            LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
            LoadType.APPEND -> getRemoteKeyForLastItem(state)?.nextKey ?: return MediatorResult.Success(true)
        }
        return try {
            val response = apiService.getProducts(page = page, pageSize = state.config.pageSize)
            val products = response.data
            val endOfPagination = products.isEmpty()

            database.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    database.remoteKeysDao().clearRemoteKeys()
                    database.productDao().clearProducts()
                }
                val keys = products.map { RemoteKeys(it.id, if (page == 1) null else page - 1, if (endOfPagination) null else page + 1) }
                database.remoteKeysDao().insertAll(keys)
                database.productDao().insertAll(products.map { it.toEntity() })
            }
            MediatorResult.Success(endOfPaginationReached = endOfPagination)
        } catch (e: Exception) {
            MediatorResult.Error(e)
        }
    }
}

مکانیزم داخلی دیتابیس Room با استفاده از کلاس InvalidationTracker بر روی جدول SQLite نظارت می کند. به محض اینکه تراکنش database.withTransaction به اتمام برسد و رکوردهای جدید در جدول محصولات ذخیره گردند، Room به طور خودکار سیگنال بی اعتباری داده ها را صادر می نماید.

این رویداد بلافاصله موجب می شود نمونه قبلی PagingSource باطل شده و یک نمونه جدید از دیتابیس ایجاد گردد که رکوردهای تازه را در قالب Flow ناهمگام به کامپوز ارسال می کند.

این معماری تضمین می نماید که داده ها حتی در ضعیف ترین اتصالات شبکه یا در زمان قطع کامل دسترسی به اینترنت، با پایداری کامل و بدون از دست رفتن موقعیت اسکرول کاربر نمایش داده شوند.

در ادامه توابع کمکی محاسبه کلید صفحات از روی رکوردهای کش شده پیاده سازی می گردند:

// توابع کمکی واکشی کلیدهای صفحه بندی از جدول RemoteKeys
private suspend fun getRemoteKeyForLastItem(state: PagingState): RemoteKeys? {
    return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()?.let { product ->
        database.remoteKeysDao().getRemoteKeysByProductId(product.id)
    }
}

private suspend fun getRemoteKeyClosestToPosition(state: PagingState): RemoteKeys? {
    return state.anchorPosition?.let { position ->
        state.closestItemToPosition(position)?.id?.let { productId ->
            database.remoteKeysDao().getRemoteKeysByProductId(productId)
        }
    }
}

اجرای کلیه عملیات حذف و درج درون بلوک database.withTransaction حیاتی است تا از ایجاد حالت های متناقض و خرابی داده های کش ممانعت به عمل آید.

۱۱. مهاجرت از Paging ۲ و لیست های سنتی به Paging ۳

بسیاری از اپلیکیشن های فعال در کافه بازار و مایکت همچنان از معماری های فرسوده Paging 2 بهره می برند.

مقایسه فنی Paging 2 و Paging 3 در اندروید
مقایسه ابعاد معماری: حذف کلاس های پراکنده DataSource و کالبک های پیچیده Paging ۲ در برابر معماری مدرن، ماژولار و متکی بر فلو در Paging ۳

مراحل اصلی بازنویسی و مهاجرت شامل موارد زیر است:

  1. حذف DataSource.Factory: کلاس های قدیمی کارخانه منبع داده را با تابع لامبدا ساده pagingSourceFactory = { MyPagingSource() } در سازنده Pager جایگزین کنید.
  2. تبدیل LivePagedListBuilder به Pager: تعریف Pager مستقیماً شیء Flow کاتلین تولید می کند و نیاز به LiveData را حذف می نماید.
  3. جایگزینی PagedListAdapter با LazyPagingItems: در کامپوز نیازی به آداپترهای سنتی نیست؛ کافی است متد collectAsLazyPagingItems() را به کار گیرید.

۱۲. تست نویسی واحد و راستی آزمایی عملکرد PagingSource

یکی از بزرگ ترین مزایای Paging 3 نسبت به نسخه های پیشین، آزمون پذیری فوق العاده آسان آن است.

با استفاده از ابزار های تستی کوروتین ها، می توان متد load را به صورت کاملاً مستقل و در محیط JVM بدون نیاز به امولاتور یا دستگاه واقعی تست کرد:

// تست واحد کلاس PagingSource در محیط آزمون کاتلین
@OptIn(ExperimentalCoroutinesApi::class)
class ProductPagingSourceTest {

    private val mockApiService = mockk()
    private val pagingSource = ProductPagingSource(mockApiService)

    @Test
    fun load_returnsSuccessResult_whenDataFetchedSuccessfully() = runTest {
        val mockData = listOf(ProductItem(1, "گوشی موبایل", 25000000.0, "img.jpg"))
        coEvery { mockApiService.getProducts(page = 1, pageSize = 20) } returns ProductApiResponse(
            data = mockData,
            totalPages = 2,
            currentPage = 1
        )

        val result = pagingSource.load(
            PagingSource.LoadParams.Refresh(
                key = null,
                loadSize = 20,
                placeholdersEnabled = false
            )
        )

        val expected = PagingSource.LoadResult.Page(
            data = mockData,
            prevKey = null,
            nextKey = 2
        )

        assertEquals(expected, result)
    }
}

۱۳. اشتباهات مهلک و خطاهای رایج پروداکشن در استفاده از Paging ۳

با وجود طراحی پیشرفته Paging 3، اشتباه در تنظیم پارامترها می تواند منجر به باگ های حافظه یا مصرف غیرعادی باتری شود:

  • فراموش کردن cachedIn در ویومدل: اگر فلو را در viewModelScope کش نکنید، با هر بار چرخش صفحه یا جابجایی تب ها، تمام صفحات دانلود شده مجدداً از سرور فراخوانی می شوند.
  • انتخاب pageSize نامناسب: تنظیم تعداد کمتر از حجم یک اسکرین (مثلاً ۴ آیتم) باعث ارسال درخواست های پیاپی می شود. مقدار بهینه معمولاً بین ۱۵ تا ۳۰ آیتم است.
  • عدم تعیین کلید منحصربه فرد (Key) در LazyColumn: عدم استفاده از شناسه آیتم باعث می شود کامپوز موقعیت اسکرول را در زمان آپدیت داده ها گم کند.
  • ایجاد نمونه های متعدد Pager: شیء Pager باید در ریپازیتوری یا ویومدل به صورت یک استریم پایدار ساخته شود و از نمونه سازی مکرر در توابع کامپوز پرهیز گردد.
توصیه طلایی معماری: در صورتی که دیتای شما سبک است و کمتر از ۵۰ آیتم در کل دیتابیس دارید، به کارگیری Paging ۳ بار پردازشی بیهوده اضافه می کند. پیجینگ ۳ اختصاصاً برای لیست های نامحدود، کاتالوگ ها و داده های دینامیک طراحی شده است.

۱۴. چک لیست ارزیابی نهایی و توصیه راهبردی تیم های مهندسی

برای انتخاب استراتژی صفحه بندی بهینه در سال ۲۰۲۶، درخت تصمیم گیری مهندسی زیر راهنمای تیم شما خواهد بود:

  • لیست های آنلاین ساده با اسکرول بی نهایت: استفاده مستقیم از PagingSource با Retrofit و Jetpack Compose.
  • اپلیکیشن های حیاتی با پشتیبانی آفلاین کامل: ترکیب RemoteMediator با دیتابیس Room.
  • تنظیمات سبک و داده های تک مقداری: استفاده از Jetpack DataStore و پرهیز از دیتابیس های حجیم.
  • کدهای قدیمی مبتنی بر Paging ۲ و کالبک: بازنویسی فوری به Paging ۳ برای کاهش بدهی فنی و بهبود سرعت اسکرول.

پرسش های متداول و نکات کلیدی

۱. مهم ترین تفاوت میان Paging ۲ و Paging ۳ در اندروید چیست؟

پیجینگ ۳ به طور کامل با کاتلین فلو و کوروتین ها بازنویسی شده است، از جت پک کامپوز به صورت نیتیو پشتیبانی می کند، کلاس های پیچیده را در یک PagingSource ادغام نموده و مدیریت خطای شبکه را به صورت خودکار انجام می دهد.

۲. نقش متد cachedIn(viewModelScope) در Paging ۳ چیست؟

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

۳. کلاس RemoteMediator چه تفاوتی با PagingSource دارد؟

کلاس PagingSource داده ها را مستقیماً از یک منبع (مانند روم یا شبکه) برای UI فراهم می کند، اما RemoteMediator به عنوان یک هماهنگ ساز در پس زمینه عمل کرده و داده ها را از وب سرویس دریافت و درون کش Room ذخیره می نماید.

۴. چگونه در Paging ۳ با Jetpack Compose لودینگ انتهای صفحه را نمایش دهیم؟

با بررسی وضعیت lazyPagingItems.loadState.append، در صورتی که در حالت LoadState.Loading باشد، می توان یک نشانگر لودینگ دایره ای در انتهای لیست رندر کرد.

۵. دلیل نیاز به جدول RemoteKeys در معماری آفلاین چیست؟

جدول RemoteKeys شناسه صفحات قبلی و بعدی هر آیتم را روی دیسک ذخیره می کند تا در صورت قطع اینترنت یا اجرای مجدد برنامه، RemoteMediator بداند درخواست بعدی باید از چه صفحه ای آغاز شود.

۶. مقدار بهینه برای پارامتر pageSize در PagingConfig چقدر است؟

مقدار بهینه معمولاً بین ۱۵ تا ۳۰ آیتم است؛ به طوری که حجم داده ها از ارتفاع صفحه نمایش گوشی بیشتر باشد تا اسکرول نرم آغاز شود اما بار اضافی به رم تحمیل نگردد.

۷. کاربرد متد getRefreshKey در PagingSource چیست؟

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

۸. چگونه دکمه تلاش مجدد (Retry) را در زمان بروز خطای شبکه فعال کنیم؟

با فراخوانی متد lazyPagingItems.retry() در رویداد کلیک دکمه، Paging 3 به صورت خودکار آخرین درخواستی را که با خطا متوقف شده بود مجدداً ارسال می کند.

۹. آیا Paging ۳ با RecyclerView های سنتی در XML سازگار است؟

بله؛ با بهره گیری از کلاس PagingDataAdapter می توان تمامی قابلیت های ناهمگام و فلو Paging 3 را درون ریسایکلرویوی های سنتی نیز پیاده سازی نمود.

۱۰. چرا استفاده از کلید با تابع itemKey در Compose LazyColumn الزامی است؟

تعیین کلید یکتا مانع از بازترکیب غیرضروری کل آیتم ها می شود، انیمیشن های جابجایی را ممکن می سازد و وضعیت اسکرول را در زمان اضافه شدن صفحات جدید حفظ می نماید.

امتیاز و نظرات

۰.۰ (۰ نظر)
برای ثبت دیدگاه باید وارد حساب کاربری شوید. ورود

هنوز نظری ثبت نشده است. اولین نفر باشید.