Skip to content

Fix(reader): preserve reading progress across library round-trips#77

Merged
Aatricks merged 1 commit into
mainfrom
fix/reader-progress-library-roundtrip
Jul 6, 2026
Merged

Fix(reader): preserve reading progress across library round-trips#77
Aatricks merged 1 commit into
mainfrom
fix/reader-progress-library-roundtrip

Conversation

@Aatricks

@Aatricks Aatricks commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Problem

Reading progress was lost when scrolling in a chapter, opening the full library screen, then returning to the reader. Intermittent, because it only triggered via the full library route (which disposes the reader composition), not the in-reader drawer, and never after a seek.

Root cause

uiState's restore anchors (scrollIndex, restoreElementKey, restoreOffsetFraction) are set at chapter-open/seek and never updated during scrolling — scrolling updates only the controller's live progressState. On return, the reader composition is recreated and runScrollRestore re-fires, replaying the frozen anchor: it snapped the view back and a post-restore emission then persisted the stale position over the good one.

Fix

  • Monotonic restoreRequestId, bumped only at genuine restore points (calculateInitialPosition, seekToProgress).
  • runScrollRestore resolves its anchor via consumeRestoreAnchor():
    • genuine load/seek → replay the uiState anchor (behavior unchanged);
    • bare recomposition (library round-trip, also rotation / other route pushes) → re-apply the live progressState position, with the from-bottom one-shot hard-nulled.

Secondary hardening

healLibraryItemForChapterList wrote a whole row via updateItem (REPLACE) outside progressMutex — a lost-update window against a concurrent progress write. It now writes only totalChapters/currentChapter/hasUpdates via targeted column UPDATEs (healChapterMetadata), matching the refresh-path pattern from #71.

Tests

./gradlew testStandardDebugUnitTest detekt → 487 tests, 0 failures, detekt clean. Adds 7 tests (anchor-resolution branches, the counter increment, the targeted metadata write); retargets one existing test from updateItem to healChapterMetadata.

The unit suite covers the decision logic; the composable's dispose→recompose replay still warrants an on-device text-chapter round-trip check.

uiState restore anchors (index, element key, offset fraction) are frozen
at chapter-open and never updated during scrolling — scrolling only
updates the controller's live progressState. Opening the full library
route disposes the reader composition; returning recomposes it and re-runs
runScrollRestore, which replayed the stale anchor: it snapped back and then
persisted the stale position over the good one. The in-reader drawer never
hit this (it keeps the reader composed), and a seek never did (it syncs
uiState), which made it intermittent.

Gate restore on a monotonic restoreRequestId bumped only at genuine restore
points (calculateInitialPosition, seekToProgress). runScrollRestore resolves
its anchor via consumeRestoreAnchor(): a genuine load/seek replays the
uiState anchor unchanged, a bare recomposition re-applies the live
progressState position with the from-bottom one-shot hard-nulled. Also
covers rotation and other route pushes, same recomposition surface.

Also harden healLibraryItemForChapterList: it wrote a whole row via
updateItem (REPLACE outside progressMutex), a lost-update window against a
concurrent progress write. It now writes only totalChapters/currentChapter/
hasUpdates via targeted column UPDATEs (healChapterMetadata).
@Aatricks Aatricks merged commit 3a1f692 into main Jul 6, 2026
4 checks passed
@Aatricks Aatricks deleted the fix/reader-progress-library-roundtrip branch July 6, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant