PagingSource and Pager
Define how pages are loaded.
PagingSource and Pager is a free Android Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Defining How Pages Load
To page from a network API you write a PagingSource. It answers two questions for Paging 3:
- How do I load the page for a given key?
- If the user refreshes, where should I resume from?
You then wrap it in a Pager that turns it into a Flow<PagingData>.
PagingSource Type Parameters
PagingSource<Key, Value> takes two type parameters:
- Key - identifies a page. For a page-number API it is
Int; for a cursor API it is aStringtoken. - Value - the item type, e.g.
Article.
import androidx.paging.PagingSource
import androidx.paging.PagingState
class ArticlePagingSource(
private val api: ArticleApi
) : PagingSource<Int, Article>() {
// implement load() and getRefreshKey()
}Implementing load()
load() is a suspend function. It receives params.key (the page to fetch) and returns a LoadResult.
On success you return LoadResult.Page with the items plus the previous and next keys. null for a key means there is no page in that direction.
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Article> {
val page = params.key ?: 1 // first load has a null key
return try {
val response = api.getArticles(page = page, size = params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.items.isEmpty()) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}Why prevKey and nextKey Matter
Paging uses nextKey to load forward as the user scrolls down, and prevKey to load backward (useful when starting in the middle of a list).
Returning null for nextKey tells Paging there are no more pages, so it stops requesting. Forgetting this can cause infinite empty requests.
// Stop forward paging when the server returns an empty page
nextKey = if (response.items.isEmpty()) null else page + 1
// Stop backward paging at the first page
prevKey = if (page == 1) null else page - 1Implementing getRefreshKey()
When the list refreshes (pull-to-refresh or invalidation), Paging needs to know which page to reload so the user stays roughly in place.
getRefreshKey() uses the current anchorPosition - the item closest to the viewport - to pick a sensible key.
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
return state.anchorPosition?.let { anchor ->
val closestPage = state.closestPageToPosition(anchor)
closestPage?.prevKey?.plus(1)
?: closestPage?.nextKey?.minus(1)
}
}Handling Errors Cleanly
Wrap the network call in a try/catch and return LoadResult.Error(e) on failure. Paging surfaces this as a LoadState.Error in the UI so you can show a retry button.
Never let an exception escape load() - catch it and convert it to LoadResult.Error.
return try {
val response = api.getArticles(page = page, size = params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.items.isEmpty()) null else page + 1
)
} catch (e: IOException) { // no network
LoadResult.Error(e)
} catch (e: HttpException) { // non-2xx response
LoadResult.Error(e)
}Creating a Pager
A Pager ties your PagingConfig to a factory that builds a fresh PagingSource. Its .flow property is a Flow<PagingData> the UI collects.
The factory lambda must create a new source each time, because Paging invalidates and recreates the source on refresh.
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
class ArticleRepository(private val api: ArticleApi) {
fun articleStream(): Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = 20, prefetchDistance = 5),
pagingSourceFactory = { ArticlePagingSource(api) }
).flow
}cachedIn for ViewModels
Collecting PagingData is a one-shot operation; re-collecting restarts loading. To survive configuration changes and let multiple collectors share data, cache the flow in the viewModelScope with cachedIn.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
class ArticleViewModel(
repo: ArticleRepository
) : ViewModel() {
val articles = repo.articleStream()
.cachedIn(viewModelScope)
}loadSize vs pageSize
Note that load() reads params.loadSize, not your configured pageSize directly.
On the very first load Paging may request a larger initial chunk (controlled by initialLoadSize in PagingConfig, defaulting to 3x page size). Always pass params.loadSize to your API so the request matches what Paging expects back.
// Correct: respect the size Paging asks for
val response = api.getArticles(page = page, size = params.loadSize)
// PagingConfig can tune the first load:
PagingConfig(pageSize = 20, initialLoadSize = 40)Cursor-Based APIs
Not every API uses page numbers. Some return a cursor or token pointing to the next page. The pattern is identical - just change the Key type to String and use the token from the response.
class CursorArticleSource(
private val api: ArticleApi
) : PagingSource<String, Article>() {
override suspend fun load(
params: LoadParams<String>
): LoadResult<String, Article> {
val cursor = params.key // null on first load
return try {
val res = api.getArticles(cursor = cursor, size = params.loadSize)
LoadResult.Page(
data = res.items,
prevKey = null, // forward-only cursor
nextKey = res.nextCursor // null when exhausted
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<String, Article>) = null
}Putting It Together
You now have the full data layer: a PagingSource that loads one page, a Pager that streams pages, and a ViewModel that caches the flow.
The UI layer simply collects viewModel.articles - which we render in the next lesson with a Compose LazyColumn.
// Data layer summary
// 1. ArticlePagingSource : PagingSource<Int, Article>
// 2. Pager(config, factory).flow -> Flow<PagingData<Article>>
// 3. ViewModel: repo.articleStream().cachedIn(viewModelScope)
// UI just collects viewModel.articlesQuick Check
In a page-number PagingSource, what does returning nextKey = null from load() signal?
Recap: PagingSource and Pager
You built the data layer for paging:
PagingSource<Key, Value>implementsload()andgetRefreshKey()load()returnsLoadResult.PagewithprevKey/nextKey, orLoadResult.Error- A
nullkey stops paging in that direction Pager(config, factory).flowproduces aFlow<PagingData>cachedIn(viewModelScope)keeps data across config changes
Next: rendering this flow in a Compose list.
Frequently asked questions
Is the “PagingSource and Pager” lesson free?
Yes — the full text of “PagingSource and Pager” is free to read here on the web, and the Android Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Android Academy course, upgrade to CoddyKit PRO.
What will I learn in “PagingSource and Pager”?
Define how pages are loaded. You practise Android Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Android Academy?
No prior experience is required. Android Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “PagingSource and Pager” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Android Academy lesson?
Yes. Every Android Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Why Paging
- PagingSource and Pager
- Paging in Compose Lists
- RemoteMediator and Caching