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:

  1. Offset-based: ?page=3&limit=20 → Simple but problematic with live data (inserts cause duplicates/skips)
  2. Cursor-based: ?after=cursor123&limit=20 → Reliable for live data, no duplicates
  3. 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

codeTap to expand ⛶
1// Paging 3 — Network-only PagingSource
2class ArticlePagingSource(
3 private val api: ArticleApi,
4 private val query: String
5) : PagingSource<Int, Article>() {
6
7 override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
8 val page = params.key ?: 1
9 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 null
15 )
16 } catch (e: Exception) {
17 LoadResult.Error(e)
18 }
19 }
20
21 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}
28
29// ViewModel with Paging
30@HiltViewModel
31class ArticleViewModel @Inject constructor(
32 private val api: ArticleApi
33) : ViewModel() {
34
35 private val _query = MutableStateFlow("")
36
37 val articles: Flow<PagingData<Article>> = _query
38 .debounce(300)
39 .flatMapLatest { query ->
40 Pager(
41 config = PagingConfig(
42 pageSize = 20,
43 prefetchDistance = 5,
44 initialLoadSize = 40
45 ),
46 pagingSourceFactory = { ArticlePagingSource(api, query) }
47 ).flow
48 }
49 .cachedIn(viewModelScope)
50
51 fun search(query: String) { _query.value = query }
52}
53
54// Compose UI with Paging
55@Composable
56fun ArticleList(viewModel: ArticleViewModel = hiltViewModel()) {
57 val articles = viewModel.articles.collectAsLazyPagingItems()
58
59 LazyColumn {
60 items(articles.itemCount) { index ->
61 articles[index]?.let { article ->
62 ArticleRow(article)
63 }
64 }
65
66 // Loading indicator
67 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:

  1. Implement a PagingSource for cursor-based pagination
  2. Add a RemoteMediator for offline-first paging with Room
  3. Handle empty states, loading, and error states in paged lists
  4. Implement pull-to-refresh with Paging 3
  5. 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.