Paging
Communityrcosteira79
Use when implementing paginated lists in Android or Compose with Paging 3 — PagingSource, Pager and PagingConfig setup, RemoteMediator for offline-first lists, LazyPagingItems and itemKey integration in LazyColumn, dynamic filters via flatMapLatest, and unit tests with TestPager and asSnapshot. Triggers include Paging 3, infinite list, infinite scroll, paginated list, LazyPagingItems, collectAsLazyPagingItems, and cachedIn.
Overview
- Repository
- rcosteira79/android-skills
- Publisher
- rcosteira79
- Stars
- 174
- Forks
- 21
- Bundled files
- 0
- License
- MIT
- Updated
- Nov 10, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/rcosteira79/android-skills.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/plugins/android-skills/skills/paging" ~/.claude/skills/paging
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/rcosteira79/android-skills.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/plugins/android-skills/skills/paging" ~/.codex/skills/paging
rm -rf "$tmp"Install the Paging agent skill from https://github.com/rcosteira79/android-skills/tree/main/plugins/android-skills/skills/paging into this project's skills folder.Instructions
Paging 3 for Android and Compose
Adapted from Meet-Miyani/compose-skill's Paging references. MIT licensed. Related: android-skills:android-data-layer (non-paged repository), android-skills:android-retrofit / android-skills:kmp-ktor (network feeding PagingSource), android-skills:compose (LazyColumn).
This reference covers the current-version fact, the placement and identity rules that are easy to get wrong, and the RemoteMediator offline-first timing — not the basics of PagingSource / Pager / collectAsLazyPagingItems.
Version (current fact)
As of androidx.paging 3.5.0, paging-common, paging-compose, and paging-testing are Kotlin Multiplatform (Android, JVM, iOS, Native, JS/Wasm); only paging-runtime (and -guava/-rxjava) stay Android-only. So in a clean-architecture domain module, depend only on androidx.paging:paging-common — it ships PagingSource / PagingData / LoadResult with no Android dependency, so PagingSource interfaces and use cases can live in domain. (Verify the current version and KMP support at the releases page.)
Dual-flow rule — PagingData is its own Flow, never a field in UiState
Expose uiState: StateFlow<UiState> AND pagingItems: Flow<PagingData<T>> as separate properties.
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
val pagingItems: Flow<PagingData<RepoUi>> =
Pager(PagingConfig(pageSize = 20, prefetchDistance = 5, enablePlaceholders = false)) { repo.repoPagingSource() }
.flow
.map { data -> data.map { it.toUi() } } // transforms BEFORE cachedIn
.cachedIn(viewModelScope)If PagingData rides inside UiState, every non-paging state change (selection, filter chip, snackbar) re-emits UiState and hands collectAsLazyPagingItems() a brand-new Flow<PagingData> — it drops its cache, restarts loads, and resets scroll to top.
cachedIn placement
cachedIn(viewModelScope) sits at the bottom of the chain: after any flatMapLatest (filters/search), and after every map / filter / insertSeparators. Anything chained after cachedIn re-runs in each collector's scope (re-transforming cached data, desyncing separators); a cachedIn placed inside a flatMapLatest lambda leaks a fresh cache on every filter change.
val pagingItems = combine(_query.debounce(300).distinctUntilChanged(), _status.distinctUntilChanged(), ::Pair)
.flatMapLatest { (q, s) -> Pager(PagingConfig(20)) { repo.repoPagingSource(q, s) }.flow.map { it.map(RepoDto::toUi) } }
.cachedIn(viewModelScope)itemKey is mandatory in LazyColumn
items(
count = pagingItems.itemCount,
key = pagingItems.itemKey { it.id }, // stable domain id
contentType = pagingItems.itemContentType { "repo" },
) { idx -> pagingItems[idx]?.let { RepoRow(it) } }Without itemKey, LazyColumn reuses slots by index — a prepended page shifts every existing item's identity, so row state / rememberSaveable / animations target the wrong slot and scroll visibly jumps. (pagingItems[i] reads and triggers a load near the edge; peek(i) reads without loading. Call retry() / refresh() from event handlers, never a composable body — and not from a LaunchedEffect(Unit), which re-triggers the load every time the screen re-enters composition. Collect with collectAsLazyPagingItems(), never collectAsStateWithLifecycle(). Show a full-screen error only when itemCount == 0; with rows already on screen, keep the list and surface the error in a snackbar or footer.)
RemoteMediator (offline-first)
With offline-first paging the Room DAO is the PagingSource and a RemoteMediator drives network refresh + DB writes. Wrap those writes in db.withTransaction { }, and clear local state on LoadType.REFRESH. Gate the full-screen loader on the combined loadState.refresh + itemCount == 0: after a Loading, the combined state waits for both source and mediator to reach NotLoading, so the remote write has landed. Read loadState.source / loadState.mediator directly only to treat them differently — e.g. keep showing cached rows when the mediator refresh fails.
Testing — TestPager / asSnapshot, never .first()
A paging flow is hot and never completes, so .first() / .toList() on it hangs. Use paging-testing:
@Test fun `source returns first page`() = runTest {
val pager = TestPager(PagingConfig(10), RepoPagingSource(FakeGitHubService(repo1, repo2), "kotlin"))
val page = pager.refresh() as PagingSource.LoadResult.Page
assertEquals(2, page.data.size); assertNull(page.prevKey); assertEquals(2, page.nextKey)
}
@Test fun `viewmodel paging flow exposes first pages`() = runTest {
val items = RepoListViewModel(FakeRepoRepository()).pagingItems.asSnapshot { scrollTo(index = 30) }
assertTrue(items.size >= 30)
}FAQ
What does Paging do?
Use when implementing paginated lists in Android or Compose with Paging 3 — PagingSource, Pager and PagingConfig setup, RemoteMediator for offline-first lists, LazyPagingItems and itemKey integration in LazyColumn, dynamic filters via flatMapLatest, and unit tests with TestPager and asSnapshot. Triggers include Paging 3, infinite list, infinite scroll, paginated list, LazyPagingItems, collectAsLazyPagingItems, and cachedIn.
Who made Paging?
rcosteira79 (a community member) publishes it in the rcosteira79/android-skills repository on GitHub.
Is Paging free?
Yes. Paging is open source, published by rcosteira79 under the MIT license.
What comes with Paging?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use Paging?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.