בקשת רשימה של 10,000 מוצרים בקריאה אחת היא טעות קלאסית שהמהנדסים שלנו רואים בכל פרויקט שלישי. ה-API מחזיר 40 MB של JSON, האפליקציה קופאת בזמן הניתוח, והמשתמש רואה מסך לבן למשך 8 שניות—ואז עוזב. אנו מיישמים פגינציה ברמת החוזה בין הלקוח לשרת: זה מפחית עומס פי 3–5 ומספק ניווט חלק ללא עיכובים. בדקו את הפגינציה של ה-API שלכם—צרו קשר לצורך ביקורת.
יישום פגינציה: Offset או Cursor?
רוב המפתחים פונים כברירת מחדל לפגינציית offset: ?page=2&limit=20. זה עובד כל עוד הנתונים סטטיים. אבל הוסיפו פיד חי שבו רשומות מוכנסות בראש, והמשתמש בעמוד 3 מפספס רשומות או רואה כפילויות: INSERT בתחילת הרשימה מזיז את כל ה-offsets. בטבלאות עם 500,000+ שורות, offset סורק עד 100,000 שורות כדי להזיז את הסמן—שאילתות לוקחות 200–400 ms.
פגינציה מבוססת cursor פותרת זאת: השרת מחזיר next_cursor, הלקוח מעביר אותו בבקשה הבאה. ה-cursor הוא טוקן אטום (בדרך כלל base64 של id + timestamp) שמקבע את המיקום במערך הנתונים. שרתי PostgreSQL מיישמים זאת עם WHERE id < :cursor ORDER BY id DESC LIMIT 20. אין כפילויות, אין פערים. מהירויות השאילתות נשארות יציבות—50–100 ms ללא קשר לגודל הטבלה.
| קריטריון | פגינציית Offset | פגינציית Cursor |
|---|---|---|
| פשטות היישום | גבוהה | בינונית (דורש cursor בשרת) |
| כפילויות/פערים בעת הוספה | כן | לא |
| ביצועים על טבלאות גדולות | מתדרדרים (O(N)) | יציבים (O(1)) |
| תמיכה בקפיצה לעמוד שרירותי | כן (page=3) | קשה (רק גלילה רציפה) |
| שמירה במטמון | קלה (עמודים לפי offset) | דורשת אחסון cursors |
למה פגינציית Cursor יעילה יותר מ-Offset?
פגינציית cursor מהירה פי 3 מ-offset על טבלאות מ-100,000 שורות—בדיקות ביצועים מראות ירידה בזמן התגובה מ-300 ms ל-90 ms. היא גם מבטיחה עקביות נתונים תחת הוספות במקביל. עבור פידי חדשות, הודעות צ'אט וקטלוגים שמתעדכנים תדיר, זו הבחירה המחייבת.
איך להגדיר מטמון עם RemoteMediator?
באנדרואיד עם Paging 3, פגינציית cursor מיושמת דרך RemoteMediator + PagingSource:
class ItemPagingSource(
private val api: ItemsApi,
private val query: String
) : PagingSource<String, Item>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, Item> {
return try {
val response = api.getItems(
cursor = params.key,
limit = params.loadSize,
query = query
)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = response.nextCursor
)
} catch (e: HttpException) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<String, Item>): String? {
return state.anchorPosition?.let { anchor ->
state.closestPageToPosition(anchor)?.nextKey
}
}
}class ItemPagingSource( private val api: ItemsApi, private val query: String ) : PagingSource<String, Item>() { override suspend fun load(params: LoadParams<String>): LoadResult<String, Item> { return try { val response = api.getItems( cursor = params.key, limit = params.loadSize, query = query ) LoadResult.Page( data = response.items, prevKey = null, nextKey = response.nextCursor ) } catch (e: HttpException) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<String, Item>): String? { return state.anchorPosition?.let { anchor -> state.closestPageToPosition(anchor)?.nextKey } } } ב-Compose מתחבר דרך LazyColumn—binding מוכן שמטפל במצבי collectAsLazyPagingItems(), Loading, Error ללא קוד נוסף.
ב-iOS, המקבילה היא פריסה קומפוזיציונית עם NotLoading ו-UICollectionViewDiffableDataSource. טעינה מוקדמת מיושמת דרך NSDiffableDataSourceSnapshot: המתודה UICollectionViewDataSourcePrefetching נקראת N תאים לפני הקצה, ומפעילה בקשה לרשת מראש. ללא טעינה מוקדמת, במהירויות גלילה מעל 500 px/s, מופיעים תאים ריקים—המשתמש ממתין 1–2 שניות לטעינה.
איך ליישם פגינציית Cursor באנדרואיד: שלב אחר שלב
- הגדירו את חוזה ה-API: השרת מחזיר
prefetchItemsAtבתגובת ה-JSON. - צרו
next_cursorשמעביר את ה-cursor ב-PagingSource<String, Item>. - חברו
loadParams.keyלמטמון לא מקוון:RemoteMediatorמביא נתונים מהרשת ושומר אותם ב-Room. - ב-
RemoteMediator, יישמוPagingSourceכדי לשחזר מיקום לאחר רענון. - הגדירו
getRefreshKeyעםLazyColumnו-collectAsLazyPagingItems()מפורש עם כפתור ניסיון חוזר.
השוואת אסטרטגיות מטמון: RemoteMediator לעומת מטמון פשוט
| פרמטר | RemoteMediator + Room | מטמון פשוט (LruCache) |
|---|---|---|
| גישה לא מקוונת | כן | לא |
| עקביות בעדכונים | ETag/Last-Modified | אין מנגנון |
| מורכבות היישום | בינונית | נמוכה |
| חיסכון בתעבורה | 60–80% | 0% |
| המלצה | פרויקטים בעומס גבוה | אבות טיפוס, נתונים סטטיים |
גלילה אינסופית ו-Pull-to-Refresh
גלילה אינסופית ללא מצב שגיאה היא בעיה אופיינית. הרשת נופלת בעמוד 5, הבקשה נתקעת, המשתמש גולל למטה וכלום לא קורה. יש צורך ב-LoadStateFooter מפורש עם כפתור ניסיון חוזר.
ב-Paging 3:
adapter.addLoadStateListener { loadState ->
binding.retryButton.isVisible = loadState.source.append is LoadState.Error
binding.progressBar.isVisible = loadState.source.append is LoadState.Loading
}Pull-to-refresh מאפס את הפגינציה לעמוד הראשון דרך LoadStateFooter—Paging 3 מבטל את adapter.addLoadStateListener { loadState -> binding.retryButton.isVisible = loadState.source.append is LoadState.Error binding.progressBar.isVisible = loadState.source.append is LoadState.Loading } ומתחיל מחדש. ב-SwiftUI, זה ה-modifier adapter.refresh() שקורא ל-PagingSource ב-TCA או מעדכן את ה-view model refreshable.
מטמון ומצב לא מקוון
Paging 3 + Room הוא השילוב הסטנדרטי של offline-first. invalidateQueries() כותב נתונים למסד הנתונים המקומי, @StateObject קורא מ-Room במקום מהרשת. המשתמש רואה נתונים גם ללא אינטרנט; נתונים חדשים נטענים ברקע.
נקודה מרכזית: אסטרטגיית ביטול המטמון. אם השרת מעדכן רשומה, העותק המקומי הופך למיושן. פתרון: RemoteMediator או PagingSource בכותבי התגובה—הלקוח שולח ETag, השרת מחזיר 304 ללא גוף אם אין שינוי. Room מעדכן רק רשומות שהשתנו דרך Last-Modified. זה מפחית את כמות הנתונים שמועברת ב-60–80%.
מה כלול בעבודה
- ביקורת על ה-API הנוכחי ומפרט חוזה הפגינציה.
- יישום פגינציית cursor/offset בצד הלקוח (אנדרואיד/iOS/קרוס-פלטפורמה).
- אינטגרציה של RemoteMediator ו-Room למצב לא מקוון.
- הגדרת טעינה מוקדמת ולוגיקת ניסיון חוזר.
- בדיקות על מכשירים אמיתיים עם סימולציית רשת חלשה.
- תיעוד קוד והמלצות לשיפור השרת.
- תמיכה לאחר היישום: שבועיים של ייעוץ.
לוחות זמנים
פגינציית offset פשוטה ללא מטמון—1–2 ימים. פגינציית cursor עם RemoteMediator, מטמון לא מקוון ולוגיקת ניסיון חוזר—3–5 ימים. העלות מחושבת באופן אישי לאחר ניתוח הדרישות וה-API הקיים. יש לנו ניסיון של 5+ שנים בפיתוח מובייל ומעל 30 פרויקטים עם פגינציה—סמכו על המומחים שלנו. הזמינו ביקורת פגינציה לאפליקציה שלכם—נמצא את הפתרון האופטימלי.
טעויות נפוצות ביישום פגינציה
- חוסר טעינה מוקדמת ב-iOS—תאים ריקים בגלילה מהירה.
- שימוש ב-offset בפידים עם הוספות תכופות—כפילויות ופערים.
- התעלמות ממצבי טעינה ושגיאה: המשתמש לא רואה אינדיקטור.
- אין מטמון—כל גלילה מפעילה בקשה לרשת.
- גודל עמוד שגוי: 5 פריטים גורם לטעינות תכופות, 100 גורם להמתנה ארוכה.







