Skip to content

♻️ Mobile | Offline Activity feed + PagedListSource pattern (exemplar for #1567)#1569

Draft
jernejk wants to merge 14 commits into
mainfrom
1567-offline-viewmodel-pattern
Draft

♻️ Mobile | Offline Activity feed + PagedListSource pattern (exemplar for #1567)#1569
jernejk wants to merge 14 commits into
mainfrom
1567-offline-viewmodel-pattern

Conversation

@jernejk

@jernejk jernejk commented Jul 3, 2026

Copy link
Copy Markdown
Member
  1. What triggered this change? (PBI link, Email Subject, conversation + reason, etc)

✏️ Part of #1567 — exemplar phase: make one page ViewModel offline-ready with a clean, replicable pattern before hardening the rest of the app.

  1. What was changed?

✏️
Activity feed now works offline (was: "Oops…" alert + empty page in airplane mode), and the plumbing it needed became a reusable pattern:

  • New PagedListSource<T> (Common/) — bindable list source owning the whole load lifecycle (caching, paging, cancellation, anti-flicker); ViewModels declare intent and handle a result — the reusable pattern for every list page.
  • ActivityPageViewModel rewritten around it — cached feed renders offline; background-refresh failures stay silent behind cached content; failed segment switches tell the user; failed LoadMore retries instead of faking "end of feed".
  • IFileCacheService gained never-throw GetAsync/SetAsync; AdvancedObservableCollection marked legacy (remaining pages migrate in ✨ Mobile | Harden offline mode – close the loop on #1306 #1567).
  • Display-field mapping → ActivityFeedItemExtensions.PrepareForDisplay (runs for cached and fresh items, so timestamps stay current).
  • ActivityPageViewModel + BaseViewModel on CommunityToolkit.Mvvm partial-property [ObservableProperty] syntax; toolkit 8.4.0 → 8.4.2 (needed for stable C# 14).
  • The replicable recipe documented in _docs/agents/maui-patterns.md.
  • New MobileUI.Core project — platform-independent pieces in one-class-per-file folders (Paging/, Collections/, Caching/), making the pattern unit-testable (the MAUI head can't be referenced by test projects).
  • 14 unit tests for PagedListSource (tests/MobileUI.Core.UnitTests) — caching, paging end-detection/retry, offline semantics, anti-flicker, stale-load cancellation.
  • Akavache v11 cache backendIFileCacheService now backed by Akavache's SQLite LocalMachine store (with optional expiry + flush-on-background); the JSON-file implementation is gone.
  • HTTP resilienceAddStandardResilienceHandler() on the typed API clients (retry + circuit breaker + timeouts); retries disabled for non-idempotent methods so quiz-submit/redeem/scan are never replayed.
  • Device-free offline teststests/MobileUI.Core.IntegrationTests (10 tests) runs REAL Akavache SQLite on the host plus a WireMock in-process API: the go-offline scenario (cache fills online → server stops → cold start serves cache + reports the error), GET-retry-on-503, and POST-never-retried are now pinned by CI (new PR jobs for both new test projects).
  • Solution migrated to SSW.Rewards.slnx (CI, rewards-dev root detection, and docs updated; .sln removed).
  • ViewModel seams (host-testable logic) — new Core interfaces IConnectivityService, IAppNavigator, IApiExceptionHandler, IAlertService with MAUI implementations; ActivityPageViewModel no longer touches any MAUI static (Shell/Connectivity/ExceptionHandler).
  • OfflineAwareListErrorHandler — the offline alert policy (401 first, silent behind cached content, offline vs generic wording) extracted into Core and covered by 7 unit tests. Note: a 401 during a background refresh now correctly triggers re-login (previously swallowed).
  • Cache wiped on logoutIFileCacheService.ResetAsync() invoked from AuthenticationService.SignOut, so no cached user data outlives the session (same concern class as 🐛 Bug - Profile and email visibility is inconsistent after app restart in offline mode #1527).
Before (offline) After (offline)
before after

Figure: Activity feed in airplane mode — before: error alert + empty page; after: feed renders from cache (no alert, no crash).

segment switch offline

Figure: Switching segments offline now explains why fresh data can't load instead of failing silently.

demo

Figure: Feed loaded online (Wi-Fi icon) → app killed, relaunched in airplane mode → same feed served from cache.

  1. Did you do pair or mob programming?

✏️ Yes — JK pairing with Claude (Fable 5).

jernejk and others added 14 commits July 3, 2026 16:09
…l declares intent only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…arate files & folders

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ine, cancellation)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the JSON-file IFileCacheService implementation with an Akavache v11
LocalMachine-backed cache, and add a standard HTTP resilience pipeline to the
typed API clients (retries limited to safe/idempotent methods).

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on, docs)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reMock API, wired into PR CI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, error, alert) in Core

Add Core interfaces so ViewModels can drop MAUI statics:
- IConnectivityService (online state + change event)
- IAppNavigator (semantic navigation, no MAUI types)
- IApiExceptionHandler (exception → handled?)
- Move IAlertService interface into Core (no MAUI-typed members)

Provide MAUI implementations (MauiConnectivityService, AppNavigator,
ApiExceptionHandler) wrapping Connectivity.Current / Shell + ActivatorUtilities /
static ExceptionHandler, and register them in DI.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…he ViewModel

Add OfflineAwareListErrorHandler (Core) capturing the decision that was inline in
ActivityPageViewModel: run the API exception handler first, stay silent when a
background refresh fails behind cached content, else alert with offline- or
generic-worded copy based on connectivity.

Refactor ActivityPageViewModel to depend on IAppNavigator + the handler only — it
now references no MAUI statics (Shell/Connectivity/ExceptionHandler). Behaviour
preserved: LoadFeed is a background refresh, a segment switch is user-requested,
and the messages are unchanged.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Cover the full decision matrix for OfflineAwareListErrorHandler with hand-rolled
fakes (no Moq): no error → silent + handler untouched; handled 401 → no alert;
background refresh behind cached content → silent; background refresh with no
content → alert; user-requested (segment switch) with cached content → still
alerts; offline routes the offline message and online routes the generic one.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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