What it is
Paging 3 loads large datasets incrementally on Android, handling page requests, retries, placeholders and Compose integration.
A PagingSource loads one page given a key. Pager turns it into a Flow<PagingData>, and the UI layer collects it with built-in loading and error states.
Installation
implementation("androidx.paging:paging-runtime:3.3.2")Getting started
The smallest useful thing you can do with it, and what each part means.
kotlin
class BookPagingSource(private val api: BookApi) : PagingSource<Int, Book>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Book> = try {
val page = params.key ?: 1
val response = api.getBooks(page, params.loadSize)
LoadResult.Page(
data = response.books,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.books.isEmpty()) null else page + 1,
)
} catch (e: IOException) {
LoadResult.Error(e) // the UI can offer retry
}
override fun getRefreshKey(state: PagingState<Int, Book>) =
state.anchorPosition?.let { state.closestPageToPosition(it)?.nextKey?.minus(1) }
}
@Composable
fun BookList(items: LazyPagingItems<Book>) {
LazyColumn {
items(items.itemCount, key = items.itemKey { it.id }) { index ->
items[index]?.let { BookRow(it) }
}
when (items.loadState.append) {
is LoadState.Loading -> item { CircularProgressIndicator() }
is LoadState.Error -> item { RetryButton(onClick = items::retry) }
else -> Unit
}
}
}Advanced usage
Where the library earns its place over a simpler alternative.
kotlin
val books: Flow<PagingData<UiBook>> = Pager(
config = PagingConfig(pageSize = 20, prefetchDistance = 5, enablePlaceholders = false),
remoteMediator = BookRemoteMediator(api, database), // network + local
pagingSourceFactory = { database.bookDao().pagingSource() },
).flow
.map { paging -> paging.map { it.toUiBook() } }
.cachedIn(viewModelScope) // survives configuration changesErrors and fixes
The failures you are most likely to hit, and what actually resolves them.
- The list reloads on every rotation
- cachedIn is missing from the Flow.
- Duplicate items appear while scrolling
- The paging keys overlap, usually an off-by-one in nextKey, or the server is not returning a stable order.
Best practices
- Always call cachedIn(viewModelScope) or rotation refetches everything.
- Return null for nextKey at the end, or the list loads forever.
- Provide a stable itemKey so scroll position survives refreshes.
- Use RemoteMediator with a local database for offline support rather than paging the network directly.
Background
Why it exists, and what it was reacting to.
Paging exists because loading a ten-thousand-row list at once exhausts memory and wastes bandwidth. Paging 3 rebuilt the API around coroutines and Flow after the first two versions proved awkward.
