Skip to content

About

Building a standalone repro backed by PokeAPI

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

Paging 3 scroll-jump — isolation test log

A minimal, self-contained Android app to isolate a Jetpack Paging 3 scroll-position bug: during pagination, a LazyColumn backed by a Room PagingSource + RemoteMediator jumps backward mid-scroll — the presented itemCount transiently collapses to ~initialLoadSize and the viewport is clamped.

This project starts from the simplest possible setup and adds one data-source characteristic at a time, scrolling on a real device after each change, to find the exact trigger.

Toolchain (fixed across all tests)

  • Paging 3.3.6, Room 2.8.4, AGP 8.9.3, Kotlin 2.1.20, Compose BOM 2026.03.01, Gradle 8.11.1
  • PagingConfig: pageSize=50, initialLoadSize=100, prefetchDistance=12, enablePlaceholders=true
  • Data: PokeAPI (/api/v2/pokemon?limit=50&offset=X) — offset pagination, no auth, ~1300 items
  • Log tag: ReproTrace — per frame visibleIndex, itemCount, maxSeen, and a <<< COLLAPSE marker when itemCount drops below the max already loaded.

Tests

# Setup Result Doc
01 Baseline — flat Room table, single-transaction RemoteMediator, SELECT * ORDER BY position No jump 01-baseline-mediator-pagingsource.md
02 + generation membership (two-table JOIN + MAX(generation) + gen-bump-on-refresh + prune) No jump 02-generation-membership.md
03 Instrumented deep-dive (timing, invalidation count, LoadStates) — same config as 02 No jump — placeholders keep itemCount = before+got+after = full COUNT 03-instrumented-why-placeholders-stay-stable.md
04 REFRESH over cached (higher-generation) data + REFRESH/CLEAR-DB controls REPRODUCED — gen-bump refresh → MAX(generation) = partial page → COUNT 150→50 → collapse 04-REPRODUCED-refresh-generation-swap.md
05 Reactive pager (flatMapLatest) + auto mid-scroll input re-emit REPRODUCED on plain scroll, no manual refresh — rebuild → new mediator launchInitialRefresh → gen bump → COUNT 200→50 → viewport jumped 145→37 05-REPRODUCED-reactive-rebuild-plain-scroll.md

Trigger (test 05) + mechanism (test 04) = the whole bug. A reactive-pager rebuild (a combine input re-emitting mid-scroll) creates a new RemoteMediator whose launchInitialRefresh bumps the generation over cached data → MAX(generation) collapses COUNT → viewport clamp → jump. Not Paging 3. Fix: (1) stop mid-scroll pager rebuilds (distinct combine inputs), (2) atomic-generation swap / gate launchInitialRefresh when cache present.

Root cause (found in test 04)

The generation swap is not atomic: MAX(generation) advances on the first new-generation write (page 0), so a REFRESH that bumps the generation while cached (higher-count) data is shown collapses COUNT(*) to the new partial page → the Paging presenter clamps the viewport → jump. Requires (1) cached data already shown + (2) a gen-bumping REFRESH. Fix: make the swap atomic (active-generation pointer that advances only on completion) and/or stop mid-scroll pager rebuilds/refreshes.

How to run a test

JAVA_HOME=<jdk17> ./gradlew :app:installDebug
adb shell am start -n com.example.pagingrepro/.MainActivity
adb logcat -c && adb logcat -v time ReproTrace:D '*:S'

Then deep-scroll 0 → ~800 and read the trace.

About

Building a standalone repro backed by PokeAPI

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Used by

Contributors

Languages