Pagination Strategies
0/3 in this phase0/52 across the roadmap
📖 Concept
Pagination is essential for loading large datasets efficiently on mobile. The Paging 3 library is Google's recommended solution for Android.
Pagination types:
- Offset-based:
?page=3&limit=20→ Simple but problematic with live data (inserts cause duplicates/skips) - Cursor-based:
?after=cursor123&limit=20→ Reliable for live data, no duplicates - Keyset-based:
?after_id=456&limit=20→ Similar to cursor but uses a data field as cursor
Paging 3 architecture:
PagingSource (data loading) → Pager → PagingData<T> → LazyColumn/RecyclerView
↑
PagingConfig (pageSize, prefetchDistance)
Key components:
- PagingSource: Loads pages of data from a single source (network or DB)
- RemoteMediator: Loads from network AND saves to DB (offline-first paging)
- Pager: Creates the PagingData stream
- PagingConfig: Controls page size, prefetch, initial load
- cachedIn(viewModelScope): Caches paging data across config changes
💻 Code Example
code
1// Paging 3 — Network-only PagingSource2class ArticlePagingSource(3 private val api: ArticleApi,4 private val query: String5) : PagingSource<Int, Article>() {67 override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {8 val page = params.key ?: 19 return try {10 val response = api.searchArticles(query, page, params.loadSize)11 LoadResult.Page(12 data = response.data.map { it.toDomain() },13 prevKey = if (page == 1) null else page - 1,14 nextKey = if (response.pagination.hasNext) page + 1 else null15 )16 } catch (e: Exception) {17 LoadResult.Error(e)18 }19 }2021 override fun getRefreshKey(state: PagingState<Int, Article>): Int? {22 return state.anchorPosition?.let { pos ->23 state.closestPageToPosition(pos)?.prevKey?.plus(1)24 ?: state.closestPageToPosition(pos)?.nextKey?.minus(1)25 }26 }27}2829// ViewModel with Paging30@HiltViewModel31class ArticleViewModel @Inject constructor(32 private val api: ArticleApi33) : ViewModel() {3435 private val _query = MutableStateFlow("")3637 val articles: Flow<PagingData<Article>> = _query38 .debounce(300)39 .flatMapLatest { query ->40 Pager(41 config = PagingConfig(42 pageSize = 20,43 prefetchDistance = 5,44 initialLoadSize = 4045 ),46 pagingSourceFactory = { ArticlePagingSource(api, query) }47 ).flow48 }49 .cachedIn(viewModelScope)5051 fun search(query: String) { _query.value = query }52}5354// Compose UI with Paging55@Composable56fun ArticleList(viewModel: ArticleViewModel = hiltViewModel()) {57 val articles = viewModel.articles.collectAsLazyPagingItems()5859 LazyColumn {60 items(articles.itemCount) { index ->61 articles[index]?.let { article ->62 ArticleRow(article)63 }64 }6566 // Loading indicator67 when (articles.loadState.append) {68 is LoadState.Loading -> item { CircularProgressIndicator() }69 is LoadState.Error -> item {70 RetryButton { articles.retry() }71 }72 else -> {}73 }74 }75}
🏋️ Practice Exercise
Practice:
- Implement a PagingSource for cursor-based pagination
- Add a RemoteMediator for offline-first paging with Room
- Handle empty states, loading, and error states in paged lists
- Implement pull-to-refresh with Paging 3
- Compare offset vs cursor pagination — when does offset break?
⚠️ Common Mistakes
Using offset pagination with live data — new items push existing items to next page, causing duplicates or skips
Not using cachedIn(viewModelScope) — paging data is lost on configuration change
Setting pageSize too small — many small network requests are worse than fewer larger ones
Not implementing getRefreshKey — pull-to-refresh doesn't scroll to the right position
💼 Interview Questions
🎤 Mock Interview
Mock interview is powered by AI for Pagination Strategies. Login to unlock this feature.
Was this topic helpful?