0Pricing
Android Academy · Lesson

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 a String token.
  • 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 - 1

Implementing 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.articles

Quick 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> implements load() and getRefreshKey()
  • load() returns LoadResult.Page with prevKey/nextKey, or LoadResult.Error
  • A null key stops paging in that direction
  • Pager(config, factory).flow produces a Flow<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

  1. Why Paging
  2. PagingSource and Pager
  3. Paging in Compose Lists
  4. RemoteMediator and Caching
← Back to Android Academy