android: keep the paged feed stable across no-op refreshes #7

Merged
salix merged 1 commit from fix/feed-scroll-state-restore into main 2026-10-18 17:00:21 +02:00
Owner

Problem

Scroll deep into the feed, leave the app for a moment, and come back: the grid has jumped towards the top and takes a long time to load the items again.

Root cause

Three stacked issues:

  1. No-op refreshes reset the feed. On every foreground pull (SyncWorker.syncConfigured(pullOnly = true)) and after every periodic feed sync, refreshCoreData() bumped the feed revision unconditionally — even when the sync merged nothing. The revision bump makes FeedScreen re-run FeedRepository.reset(...).
  2. reset() truncated the loaded list. A same-query refresh replaced the loaded rows with only the first page (48 rows), so a deeply scrolled grid clamped towards the top.
  3. Restoring a deep position was slow. The scroll-anchor logic calls loadUntil(anchor), which paged 48 rows at a time through the single serialized core lane — one round-trip per page — and each page fetch ran inside the anchor LaunchedEffect, which restarts when loading toggles and could cancel the in-flight fetch.

Fix

  • SyncWorker.syncConfigured now skips refreshCoreData() when the sync's SyncMergeStats are all zero: a routine foreground pull or no-op push can no longer disturb the feed.
  • FeedRepository.reset() keeps the already-loaded rows when the fresh first page is identical to their prefix (feed content unchanged); any real change still falls through to the fresh page.
  • FeedRepository.loadUntil() fetches a single batch sized to cover the anchor (one core round-trip instead of one per page), and FeedScreen launches the restore on a composition scope so it survives the effect's loading-triggered restarts.

Testing

  • FeedRepositoryRoutingTest: updated loadUntilPagesDownToTheTargetIndex for batched fetching and added loadUntilCoversADeepAnchorWithOneBatchedRequest, sameQueryResetKeepsPagedRowsWhenTheFeedDidNotChange, and sameQueryResetReplacesRowsWhenTheFeedChanged. All pass.
  • The remaining unit-test failures (UniffiLib/libinvio_core.so not loadable in the unit-test JVM) are pre-existing on main and unrelated.
## Problem Scroll deep into the feed, leave the app for a moment, and come back: the grid has jumped towards the top and takes a long time to load the items again. ## Root cause Three stacked issues: 1. **No-op refreshes reset the feed.** On every foreground pull (`SyncWorker.syncConfigured(pullOnly = true)`) and after every periodic feed sync, `refreshCoreData()` bumped the feed revision unconditionally — even when the sync merged nothing. The revision bump makes `FeedScreen` re-run `FeedRepository.reset(...)`. 2. **`reset()` truncated the loaded list.** A same-query refresh replaced the loaded rows with only the first page (48 rows), so a deeply scrolled grid clamped towards the top. 3. **Restoring a deep position was slow.** The scroll-anchor logic calls `loadUntil(anchor)`, which paged 48 rows at a time through the single serialized core lane — one round-trip per page — and each page fetch ran inside the anchor `LaunchedEffect`, which restarts when `loading` toggles and could cancel the in-flight fetch. ## Fix - `SyncWorker.syncConfigured` now skips `refreshCoreData()` when the sync's `SyncMergeStats` are all zero: a routine foreground pull or no-op push can no longer disturb the feed. - `FeedRepository.reset()` keeps the already-loaded rows when the fresh first page is identical to their prefix (feed content unchanged); any real change still falls through to the fresh page. - `FeedRepository.loadUntil()` fetches a single batch sized to cover the anchor (one core round-trip instead of one per page), and `FeedScreen` launches the restore on a composition scope so it survives the effect's `loading`-triggered restarts. ## Testing - `FeedRepositoryRoutingTest`: updated `loadUntilPagesDownToTheTargetIndex` for batched fetching and added `loadUntilCoversADeepAnchorWithOneBatchedRequest`, `sameQueryResetKeepsPagedRowsWhenTheFeedDidNotChange`, and `sameQueryResetReplacesRowsWhenTheFeedChanged`. All pass. - The remaining unit-test failures (`UniffiLib`/`libinvio_core.so` not loadable in the unit-test JVM) are pre-existing on `main` and unrelated.
Returning to the app after scrolling deep into the feed jumped the grid
towards the top and crawled while reloading. Three causes, all fixed:

- SyncWorker.refreshCoreData() after every sync run bumped the feed
  revision even when the run merged nothing locally. Skip the refresh
  when the merge stats are all zero so a routine foreground pull never
  resets the feed screens' paged cursors.
- FeedRepository.reset() replaced the loaded list with page one on every
  same-query refresh. When the fresh first page still matches the rows
  on screen the feed content did not change, so keep the deeper pages
  (and the scroll position) instead of clamping the grid.
- loadUntil() restored deep positions one 48-row page at a time through
  the serialized core lane. Fetch a single batch sized to cover the
  anchor instead, and run it on a composition scope so the fetch is not
  cancelled by the anchor effect restarting on its own loading toggle.
salix merged commit 3c2b5b70ce into main 2026-10-18 17:00:21 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
salix/invio!7
No description provided.