Implementing Data Pagination in Mobile Application
Requesting a list of 10,000 items in one call — classic mistake you see in every third project. API returns 40 MB JSON, app hangs on parsing, user sees white screen for 8 seconds and leaves. Pagination solves this not at UI level, but at client-server contract level.
Offset vs Cursor: Where to Use What
Most developers default to offset pagination — ?page=2&limit=20. Works while data static. Add live feed with records inserting at beginning, user on page 3 misses records or sees duplicates: INSERT at start shifts all offsets.
Cursor pagination solves the problem: server returns next_cursor, client sends it next request. Cursor is opaque token (usually base64 from id + timestamp), fixing position in dataset. PostgreSQL backends implement via WHERE id < :cursor ORDER BY id DESC LIMIT 20. No duplicates, no misses.
On Android with Paging 3, cursor pagination via 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
}
}
}
LazyColumn in Compose connects via collectAsLazyPagingItems() — ready binding handling Loading, Error, NotLoading states without extra code.
On iOS — compositional layout with UICollectionViewDiffableDataSource and NSDiffableDataSourceSnapshot. Prefetch via UICollectionViewDataSourcePrefetching: prefetchItemsAt called N cells before edge, allows network request in advance. Without prefetch at scroll speed >500 px/s you see blank cells — user waits for load.
Infinite Scroll and Pull-to-Refresh
Infinite scroll without error state — typical problem. Network drops on page 5, request hangs, user scrolls down and nothing happens. Need explicit LoadStateFooter with retry button.
In 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 resets pagination to first page via adapter.refresh() — Paging 3 invalidates PagingSource and starts over. On SwiftUI it's refreshable modifier, calling invalidateQueries() in TCA or updating @StateObject viewmodel.
Cache and Offline
Paging 3 + Room — standard stack for offline-first. RemoteMediator writes to local DB, PagingSource reads from Room, not network. User sees data even offline, fresh data loads in background.
Key point: cache invalidation strategy. If server updates record, local copy stales. Solution — ETag or Last-Modified in response headers: client sends If-None-Match, server returns 304 without body if unchanged. Room updates only changed records via @Insert(onConflict = OnConflictStrategy.REPLACE).
Timeline
Simple offset pagination without cache: 1–2 days. Cursor pagination with RemoteMediator, offline cache and retry logic: 3–5 days. Cost calculated individually after analyzing requirements and existing API.







