ترقيم الصفحات هو تقنية تحميل البيانات على دفعات، تُستخدم في التطبيقات المحمولة وخدمات الويب للعمل مع مجموعات كبيرة من السجلات. وفقًا لوثائق Android Developers (2025)، فإن التنفيذ الصحيح لترقيم الصفحات يقلل الحمل على API، ويوفر حركة المرور، ويحسن تجربة المستخدم. التحميل صفحة بصفحة يسمح للتطبيق بعرض المحتوى تدريجيًا دون انتظار تحميل جميع البيانات دفعة واحدة.
الخلاصة
ترقيم الصفحات (من الكلمة اللاتينية pagination — التقسيم إلى صفحات) هو تقنية تقسيم مجموعة بيانات كبيرة إلى أجزاء متسلسلة (صفحات). في التطبيقات المحمولة، يُستخدم ترقيم الصفحات عند تحميل قوائم الرسائل، خلاصات الأخبار، كتالوجات المنتجات، سجل الطلبات وأي مجموعات أخرى ذات عدد غير محدود من السجلات.
بدون ترقيم الصفحات، يضطر التطبيق إلى تحميل جميع البيانات دفعة واحدة، مما يؤدي إلى انتظار طويل، واستهلاك كبير لحركة المرور، وأداء غير مستقر على الأجهزة الضعيفة. طلب API مع ترقيم الصفحات يُرجع دفعة واحدة فقط من البيانات ومعلومات وصفية لتحميل الدفعة التالية — وبالتالي يتحكم التطبيق في كمية المعلومات المستلمة.
المقاييس الرئيسية لترقيم الصفحات: حجم الصفحة (page size) — عدد السجلات في الصفحة الواحدة (عادة 10–50)، ورقم الصفحة أو المؤشر — مؤشر على الموضع الحالي في المجموعة. يعتمد اختيار حجم الصفحة على نوع البيانات: للعناصر المضغوطة (الأسماء) يكفي 20–30، للبطاقات ذات الصور — 10–15.
الأجهزة المحمولة ذات موارد محدودة: حجم الذاكرة العشوائية، سرعة المعالج وحدود حركة المرور. يحل ترقيم الصفحات ثلاث مهام رئيسية: تقليل استهلاك الذاكرة (تُخزّن في الذاكرة العناصر المرئية فقط)، تسريع العرض الأول (الدفعة الأولى تُحمّل أسرع من المجموعة بأكملها)، وتوفير حركة المرور (تُحمّل البيانات فقط عندما يمرر المستخدم القائمة).
هناك أربعة أنواع رئيسية لترقيم الصفحات، كل منها يحل مهامًا محددة. يعتمد اختيار الطريقة على متطلبات اتساق البيانات، وهندسة API، ونوع التخزين، والتعقيد المقبول للتنفيذ على العميل والخادم.
| النوع | مبدأ العمل | الاستقرار | السرعة على الأحجام الكبيرة |
|---|---|---|---|
| Offset | LIMIT + OFFSET في SQL | منخفض | تتراجع مع نمو OFFSET |
| Cursor | WHERE id > last_id | مرتفع | مستقر (O(log n)) |
| Keyset | WHERE key > last_key | مرتفع | مستقر (O(log n)) |
| Time-based | WHERE created_at < last_time | متوسط | مستقر مع فهرس |
ترقيم الصفحات Offset مناسب لمجموعات البيانات الثابتة أو التي نادرًا ما تُحدّث، عندما تكون البساطة في التنفيذ مهمة. Cursor وKeyset مناسبان للبيانات الديناميكية مع الإضافات المتكررة. Time-based مناسب للخلاصات الزمنية حيث تُرتّب السجلات حسب وقت الإنشاء. معيار GraphQL Relay يستخدم ترقيم الصفحات بالمؤشر كالطريقة الوحيدة الموصى بها.
ترقيم الصفحات Offset هو أبسط أنواع التحميل الصفحي. يرسل العميل معاملات page وlimit (أو offset وlimit)، ويطبق الخادم SQL OFFSET وLIMIT. على سبيل المثال، page=2, limit=20 يُرجع السجلات من 21 إلى 40. هذه الطريقة بديهية وسهلة التنفيذ على أي stack.
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
async def get_items(
page: int = Query(default=1, ge=1),
limit: int = Query(default=20, le=100)
):
offset = (page - 1) * limit
items = await fetch_items(offset, limit)
total = await count_items()
return {
"items": items,
"total": total,
"page": page,
"pages": (total + limit - 1) // limit
}
العيب الرئيسي لترقيم الصفحات Offset هو مشكلة السجلات المفقودة والمكررة. إذا تمت إضافة سجلات جديدة إلى الجدول بين طلبين، ينزاح OFFSET: قد يرى المستخدم نفس السجل مرتين أو يفوت سجلاً جديدًا. هذا أمر بالغ الأهمية لخلاصات الأخبار والدردشات حيث الاتساق مهم.
مشكلة أخرى هي تدهور الأداء مع قيم OFFSET الكبيرة. تضطر قاعدة البيانات إلى مسح وتجاوز أول offset سجل قبل إرجاع النتيجة. عند offset=100000 حتى مع LIMIT 20، سيقضي الخادم وقتًا ملحوظًا في المسح. PostgreSQL وMySQL تُظهر انخفاضًا خطيًا في السرعة مع نمو OFFSET.
يظل ترقيم الصفحات Offset الخيار الأفضل لـ: لوحات الإدارة (تتغير البيانات نادرًا، هناك حاجة للتنقل بين الصفحات)، التقارير وسجلات التاريخ (لقطة ثابتة للبيانات)، الكتالوجات مع التصفية (يمكن الانتقال إلى أي صفحة). Offset أيضًا الأسهل في التنفيذ على العميل — RecyclerView مع Paging 3 يدعمه مباشرة.
ترقيم الصفحات Keyset يستخدم مفتاحًا فريدًا (عادة المفتاح الأساسي) لتصفية السجلات. بدلاً من OFFSET، يستخدم الاستعلام WHERE id > last_seen_id. وهذا يضمن أداءً ثابتًا بغض النظر عن عدد السجلات وعدم وجود تكرارات عند الإضافات، حيث أن السجلات الجديدة دائمًا لها id أكبر.
-- ترقيم الصفحات Offset (إشكالي)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;
-- ترقيم الصفحات Keyset (مستقر)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;
ترقيم الصفحات Time-based (أو المؤشر حسب الوقت) يستخدم الطابع الزمني created_at للتنقل. يرسل العميل الطابع الزمني لآخر سجل تم تحميله، ويعيد الخادم السجلات التي تم إنشاؤها قبل أو بعد هذا الطابع. هذه الطريقة شائعة في الشبكات الاجتماعية وخلاصات الأخبار حيث يُحدد ترتيب السجلات بوقت النشر.
من خصائص ترقيم الصفحات Time-based احتمال التكرارات إذا تم إنشاء سجلين في نفس المللي ثانية. للقضاء على هذه المشكلة، يُدمج مفتاح زمني مع id فريد: WHERE (created_at, id) < (last_time, last_id). يضمن هذا المؤشر المركب تفرد كل سجل وترتيبًا دقيقًا.
يتطلب ترقيم الصفحات Keyset عمودًا بقيمة فريدة ومتزايدة بشكل رتيب (id تلقائي الزيادة، UUID v7). Time-based مناسب لأي جدول به created_at، لكنه يتطلب معالجة إضافية للتكرارات. الفرق الرئيسي: Keyset يعمل بثبات تحت أي عمليات إدراج، بينما Time-based حساس للطوابع الزمنية المتطابقة.
يعتمد اختيار نوع ترقيم الصفحات على طبيعة البيانات ومتطلبات تجربة المستخدم. فيما يلي توصيات للسيناريوهات النموذجية في تطوير التطبيقات المحمولة. لا يوجد حل عالمي — لكل طريقة مجال تكون فيه مثالية.
مكتبة Android Paging 3 تدعم جميع أنواع ترقيم الصفحات عبر PagingSource. لـ Offset — PagingSource بمفتاح Int (page)، لـ Cursor — بمفتاح String أو Long (cursor). يدير PagingSource تلقائيًا التحميل والتخزين المؤقت وإعادة المحاولة عند الأخطاء.
class PostPagingSource(
private val api: PostApi
) : PagingSource<Long, Post>() {
override suspend fun load(
params: LoadParams<Long>
): LoadResult<Long, Post> {
val cursor = params.key ?: Long.MAX_VALUE
return try {
val response = api.getPosts(cursor, params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = response.items.lastOrNull()?.id
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Long, Post>): Long? {
return state.anchorPosition?.let {
state.closestItemToPosition(it)?.id
}
}
}
يؤثر حجم الصفحة على سرعة التحميل وإدراك الأداء. للتطبيقات المحمولة، النطاق الأمثل هو 10–25 عنصرًا لكل صفحة. أقل من 10 يؤدي إلى طلبات كثيرة جدًا لـ API وتمريرًا متقطعًا. أكثر من 25 يؤدي إلى تحميل بطيء للدفعة الأولى على الشبكات البطيئة.
للصور والفيديو، يُقلّص حجم الصفحة إلى 5–10، لأن كل عنصر يتطلب وقتًا إضافيًا لتحميل الوسائط. للقوائم النصية (التعليقات، السجلات)، يمكن زيادة الحجم إلى 30–50 سجلاً. يُوصى بجعل حجم الصفحة قابلًا للتعديل عبر API حتى يتمكن العميل من التكيف مع ظروف الشبكة المختلفة.
الأسئلة الشائعة
ترقيم الصفحات هو تحميل البيانات على أجزاء وليس كلها دفعة واحدة. كما في الكتاب: تقرأ صفحة واحدة ثم تقلب إلى التالية. في التطبيق، يعني ذلك أنه عند تمرير القائمة، تُحمّل الدفعة التالية من البيانات، وليس القائمة بأكملها، مما يوفر حركة المرور والذاكرة.
Offset يعد السجلات: «تجاوز 20، أعد الـ10 التالية». إذا أُضيف سجل جديد بين التحميلات، يختل الترقيم. Cursor يستخدم المعرف الفريد لآخر سجل: «أعد 10 سجلات بعد ID = 100». السجلات الجديدة لا تؤثر على الموضع.
للتطبيقات المحمولة، الأمثل هو 10–25 عنصرًا. للقوائم ذات الصور — 5–10، للخلاصات النصية — 20–30. يعتمد الحجم على متوسط حجم كل عنصر: كلما كان العنصر أثقل، يجب أن تكون الصفحة أصغر للعرض السريع.
استخدم مكتبة Paging 3 من Android Jetpack. توفر PagingSource للتحميل، وPagingData للتدفق التفاعلي، وPagingDataAdapter للتحميل التلقائي عند التمرير. تدعم المكتبة ترقيم الصفحات Offset وCursor وKeyset من خلال PagingSource مخصص.
التمرير اللانهائي هو نمط واجهة مستخدم حيث تُحمّل دفعة جديدة من البيانات تلقائيًا عند الاقتراب من نهاية القائمة. ترقيم الصفحات هو الآلية لتحميل البيانات على دفعات، والتمرير اللانهائي هو إحدى طرق عرضه. البديل هو زر «تحميل المزيد».
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا