PagingSource, Pager, PagingData, and refresh keys
Pager -> creates PagingSource -> LoadParams(key,size,type) -> LoadResult
-> Flow<PagingData<Item>> -> UI adapter/LazyPagingItems
PagingSource<Key, Value> loads one source. Pager controls configuration/generations and exposes Flow. PagingData is a generation of incremental events, not a normal list.
class SearchPagingSource(
private val api: SearchApi,
private val query: String,
) : PagingSource<String, Product>() {
override suspend fun load(params: LoadParams<String>): LoadResult<String, Product> = try {
val page = api.search(query, cursor = params.key, limit = params.loadSize)
LoadResult.Page(
data = page.items.map(ProductDto::toDomain),
prevKey = page.previousCursor,
nextKey = page.nextCursor,
)
} catch (e: CancellationException) {
throw e
} catch (e: IOException) {
LoadResult.Error(e)
}
override fun getRefreshKey(state: PagingState<String, Product>): String? =
state.anchorPosition?.let(state::closestPageToPosition)?.prevKey
}
Cursor refresh key depends on server contract; blindly returning previous cursor may not center correctly. For positional keys, derive around anchor; Room can handle refresh positioning.
PagingConfig
pageSize: backend/query efficient sizeprefetchDistance: how early to load near edgeinitialLoadSize: often multiple pagesmaxSize: in-memory page retention; must allow enough around viewport/prefetch- placeholders: require count support/stable positions and UI placeholder design
Backend may return fewer than requested. loadSize is a hint. End reached is usually nextKey == null/protocol marker, not simply items.size < requested unless API guarantees.
Invalidation creates a new generation
A PagingSource is single-use generation. Room invalidates on relevant table changes; Pager creates a new source. Custom source calls invalidate() when its underlying dataset becomes obsolete.
Query switching
val products = query
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { q -> repository.search(q) }
.cachedIn(viewModelScope)
flatMapLatest cancels stale generation. Keep query in Pager/PagingSource identity; reusing source with mutable query causes mixed pages.
Duplicate/missing pages usually come from key/cursor assumptions, unstable ordering, or concurrent dataset changes—not Compose list code.
Sources
Exercises
- Implement page-number and cursor key sources.
- Test API returning fewer items before true end.
- Explain why each query needs a new Pager generation.