Android Academy · Lesson

PagingSource and Pager

Define how pages are loaded.

Lesson 2 of 413 steps

PagingSource and Pager is a free Android Academy lesson on CoddyKit. This is lesson 2 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the Android Academy learning path, and your progress syncs across the web and the CoddyKit app. The Android Academy course includes 4 lessons in total.

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.

Free to start

Learn Kotlin with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
36
Lessons
152

Frequently Asked Questions

Is the “PagingSource and Pager” lesson free?

Yes — any 3 lessons of the Android Academy learning path, including “PagingSource and Pager”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The Android Academy course includes 4 lessons in total.

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, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.

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