Skip to content

fix(mobile): load tab screens once, not on every tab switch - #183

Merged
rghvgrv merged 1 commit into
mainfrom
fix/mobile-tab-switch-reload
Aug 13, 2026
Merged

fix(mobile): load tab screens once, not on every tab switch#183
rghvgrv merged 1 commit into
mainfrom
fix/mobile-tab-switch-reload

Conversation

@rghvgrv

@rghvgrv rghvgrv commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

The bug

Every tab tap refetched the whole screen. Shell raises OnAppearing on each tab selection, and all five tab pages called LoadCommand there unconditionally — a full API round trip, collections cleared, list repainted. On a phone talking to a slow or unreachable API that is a spinner every single time you come back to a screen, which is what "the app keeps refreshing" turned out to mean.

The fix

OnAppearing now calls EnsureLoadedCommand — fetch on the first visit, then leave the screen alone. Freshness comes from invalidation, not repetition:

  • SubscriptionsChangedMessage marks the list stale, so the reload happens when the user is actually looking at it rather than behind their back. The dashboard — a singleton whose totals move with any write — still refetches immediately.
  • Category and payment-source renames/deletes now publish that message. The dashboard names both in its breakdowns and no longer refetches on tab switch, so the change has to be announced. Needed an IMessenger on those two view models.
  • SessionEndedMessage clears each screen and re-arms its fetch. Shell keeps the page it built for each ShellContent, so without this the next user to sign in gets handed the previous one's rows and no request follows.
  • Pull-to-refresh is untouched: RefreshView still binds LoadCommand, which always fetches.
  • A load that ended with nothing on screen is not "loaded" — an error screen still retries on the next visit. Cached rows do count as loaded, which is exactly the case the refetch made painful.

Also brings the Payment rows in line with Categories after #179: the ••• button is now the same vertical-dots tonal icon button, and the SwipeView is gone. Two routes into one menu is one more than the screen needs, and only one of them was ever discoverable.

Verified on a physical device

Built and installed on a Pixel-class Android phone against a local API over adb reverse, then:

  • Visited all five tabs once, killed the tunnel (adb reverse --remove, device curl000), and switched tabs again. Categories and Payment still rendered in full — neither has a local cache, so that data could only have come from the first load. Subs showed its rows with no offline banner, meaning no fetch was even attempted.
  • Traced the load paths through logcat over two full tab cycles: second pass logged isLoaded=True on every screen, with no view-model reconstruction.
  • Scroll position survives leaving and returning to a tab.
  • Menu still opens anchored to its row on both screens; swiping a Payment row now does nothing.

Tests

dotnet test tests/SubVora.Mobile.Tests315 passed, 0 failed, including a new TabSwitchReloadTests covering: repeat appearance costs no call, pull-to-refresh still fetches every time, a change message re-arms the fetch, sign-out clears and refetches, cache-served counts as loaded, an empty failure retries, and both renames announce themselves.

Note

What is left after this is not a refresh: the tab bar highlights instantly while the page content swaps ~0.3–0.5s later. That is Shell's page-swap cost on Android in a Debug build, not a reload — no request is made and no view model is rebuilt. Worth judging on a Release build before chasing further.

Shell raises OnAppearing on every tab selection, and all five tab pages
called LoadCommand there unconditionally - so each tab tap cost a full
API round trip, cleared the collections and repainted them. Against a
slow or unreachable API that is a spinner every time the user comes back
to a screen, which reads as an app permanently refreshing itself.

Each page's OnAppearing now calls EnsureLoadedCommand: fetch on the first
visit, then leave the screen alone. Freshness comes from invalidation
rather than repetition:

- SubscriptionsChangedMessage marks the list stale, so the reload happens
  when the user is looking at that screen instead of behind their back.
  The dashboard, a singleton whose totals move with any write, refetches
  straight away as before.
- Category and payment-source renames and deletes now publish that
  message too. The dashboard names both in its breakdowns and no longer
  refetches on tab switch, so the change has to be announced.
- SessionEndedMessage clears each screen and re-arms its fetch. Shell
  keeps the page it built for each ShellContent, so without this the next
  user to sign in would be handed the previous one's rows and no request
  would follow.
- Pull-to-refresh is unchanged: RefreshView still binds LoadCommand,
  which always fetches.
- A load that ended with nothing on screen is not "loaded", so an error
  screen still retries on the next visit. Cached rows do count, which is
  the case the refetch made painful.

Also brings the Payment rows in line with Categories after #179: the
"..." button is the same vertical-dots tonal icon button, and the
SwipeView is gone. Two ways into one menu is one more than the screen
needs, and only one of them was ever discoverable.
@rghvgrv rghvgrv self-assigned this Aug 13, 2026
@rghvgrv
rghvgrv merged commit 9a8ec23 into main Aug 13, 2026
4 checks passed
@rghvgrv
rghvgrv deleted the fix/mobile-tab-switch-reload branch August 13, 2026 21:38
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