PagingSource and Pager
Why Paging?
Loading an entire dataset into memory wastes RAM and causes slow list scrolling. Paging loads data in pages as the user scrolls, keeping memory usage constant regardless of dataset size.
PagingSource
PagingSource defines how to load pages of data. Each page load returns a LoadResult:
class ArticlePagingSource(
private val api: ArticleApi
) : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val page = params.key ?: 1
return try {
val response = api.getArticles(page = page, pageSize = params.loadSize)
LoadResult.Page(
data = response.articles,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.articles.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}
}
Key points:
params.keyis the page number (or cursor). Start withnullfor the first page.params.loadSizeis the number of items to request.- Return
prevKey = nullfor the first page andnextKey = nullwhen no more data exists. getRefreshKeydetermines where to resume when the user scrolls back to the top.
Pager Configuration
val articlesPager = Pager(
config = PagingConfig(
pageSize = 20,
prefetchDistance = 10,
enablePlaceholders = false,
initialLoadSize = 40
),
pagingSourceFactory = { ArticlePagingSource(api) }
).flow
pageSize: items per page requestprefetchDistance: how many items before the end to trigger loadingenablePlaceholders: show empty slots for known-size listsinitialLoadSize: items in the first load (typically 2x pageSize)
Database-Backed Paging
For offline-first apps, use RoomPagingSource:
@Dao
interface ArticleDao {
@Query("SELECT * FROM articles ORDER BY publishedAt DESC")
fun getArticlesPaged(): PagingSource<Int, Article>
}
val dbArticlesPager = Pager(
config = PagingConfig(pageSize = 20, prefetchDistance = 10),
pagingSourceFactory = { articleDao.getArticlesPaged() }
).flow
Room returns a PagingSource directly, and the Paging library handles invalidation when data changes.
Displaying Paged Data in Compose
collectAsLazyPagingItems
In Compose, collect paged data as lazy items:
@Composable
fun ArticleListScreen(viewModel: ArticleViewModel = hiltViewModel()) {
val articles = viewModel.articlesPager.collectAsLazyPagingItems()
LazyColumn {
items(
count = articles.itemCount,
key = articles.itemKey { it.id }
) { index ->
val article = articles[index]
article?.let { ArticleCard(article = it) }
}
// Loading indicator at the end
when (articles.loadState.append) {
is LoadState.Loading -> {
item { LoadingIndicator() }
}
is LoadState.Error -> {
item { ErrorMessage(onRetry = { articles.retry() }) }
}
is LoadState.NotLoading -> {}
}
}
}
Load States
Paging 3 exposes three load states:
loadState.refresh: initial load or full refreshloadState.append: loading the next pageloadState.prepend: loading the previous page (rare)
Each state is one of: Loading, NotLoading(endOfPaginationReached), or Error(throwable).
Loading and Error UI
@Composable
fun ArticleListScreen(viewModel: ArticleViewModel = hiltViewModel()) {
val articles = viewModel.articlesPager.collectAsLazyPagingItems()
when (articles.loadState.refresh) {
is LoadState.Loading -> {
CircularProgressIndicator(modifier = Modifier.fillMaxSize())
}
is LoadState.Error -> {
ErrorMessage(
message = (articles.loadState.refresh as LoadState.Error).error.localizedMessage ?: "Error",
onRetry = { articles.retry() }
)
}
is LoadState.NotLoading -> {
LazyColumn {
items(count = articles.itemCount) { index ->
articles[index]?.let { ArticleCard(article = it) }
}
}
}
}
}
Invalidating the Pager
When the underlying data changes (e.g., new articles available), invalidate the PagingSource:
class ArticleViewModel(private val api: ArticleApi) : ViewModel() {
val articlesPager = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { ArticlePagingSource(api) }
).flow.cachedIn(viewModelScope)
fun refresh() {
articlesPager.invalidate()
}
}
cachedIn(viewModelScope) caches the Pager output in the ViewModel's scope, so multiple collectors share the same data.
Quiz
1. What does PagingSource.getRefreshKey return?
2. What does the prefetchDistance parameter in PagingConfig control?
3. What is the purpose of cachedIn(viewModelScope)?
4. When should you invalidate a PagingSource?
Flashcards
Question
What is a PagingSource and what does it return?
Click to reveal answer
Answer
A PagingSource defines how to load pages of data. Its load() method returns LoadResult.Page with data, prevKey, and nextKey, or LoadResult.Error on failure.
Question
What are the three load states in Paging 3?
Click to reveal answer
Answer
refresh (initial load), append (next page), and prepend (previous page). Each can be Loading, NotLoading, or Error.
Question
How do you display paged data in a LazyColumn?
Click to reveal answer
Answer
Collect with collectAsLazyPagingItems(), then use items(count = pager.itemCount) with articles[index] to access each item.
Question
What does cachedIn do for a Pager?
Click to reveal answer
Answer
Caches the Pager output in a CoroutineScope so multiple collectors share the same data. Prevents duplicate network requests.
Revision Notes
Key Takeaways
- 1. PagingSource defines how to load pages; return LoadResult.Page with prevKey/nextKey
- 2. PagingConfig controls page size, prefetch distance, and initial load size
- 3. collectAsLazyPagingItems integrates paging data into LazyColumn composables
- 4. LoadState tells you whether refresh, append, or prepend is loading, done, or errored
- 5. cachedIn(viewModelScope) caches and shares paged data across multiple collectors
Interview Tips
- • Explain the PagingSource load cycle: load -> Page/Error -> next load
- • Discuss how getRefreshKey determines where to resume after refresh
- • Know how to handle loading and error states in the UI with LoadState
- • Be ready to design an infinite scroll list with Paging 3 and LazyColumn
Cheat Sheet
Paging 3 Cheat Sheet
PagingSource:
- Extend
PagingSource<Key, Value> load(params)returnsLoadResult.Page(data, prevKey, nextKey)orLoadResult.ErrorgetRefreshKey(state)returns resume position- Return
nextKey = nullwhen no more data
Pager:
Pager(config, pagingSourceFactory)creates a Flow of paging datapageSize: items per pageprefetchDistance: trigger distance from endinitialLoadSize: first page size (default 2x pageSize)
Compose Integration:
collectAsLazyPagingItems()— collects as LazyPagingItemsitems(count = pager.itemCount)— renders itemsarticles[index]— null if not loaded, item if loadedarticles.loadState.refresh/append— loading states
Load States:
LoadState.Loading— fetching dataLoadState.NotLoading(endOfPaginationReached)— doneLoadState.Error(exception)— failure, use retry()
Caching:
.cachedIn(viewModelScope)— shares data across collectors- Prevents duplicate requests on recomposition
Invalidation:
pagingSource.invalidate()— force reload from scratch- Use when underlying data changes