critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

A `TabStateManager` Riverpod `AsyncNotifier<TabState>` (or `StateNotifier<TabState>`) is implemented in `lib/navigation/tab_state_manager.dart`
`TabState` is an immutable value class with fields: `int activeTabIndex` (0–4) and `List<NavigationStackSnapshot> snapshots` (length 5)
On `build()` / initialization, the notifier reads from `NavigationStateRepository` and populates `TabState` — widget tree shows a loading indicator until hydration is complete
`setActiveTab(int index)` validates index is 0–4, throws `RangeError` otherwise, updates `activeTabIndex` in state, and triggers async persist
`updateStackSnapshot(int tabIndex, NavigationStackSnapshot snapshot)` replaces the snapshot at `tabIndex` in the list and triggers async persist
`getSnapshot(int tabIndex)` returns the current in-memory snapshot for the given tab — synchronous access, no async required
Persistence is fire-and-forget asynchronous — mutations to in-memory state are immediately available; persist failures are logged but do not throw
Concurrent mutations are safe — if `setActiveTab` and `updateStackSnapshot` are called within the same frame, both mutations are applied and a single persist call is made (debounce or sequential queue)
A `tabStateManagerProvider` is exported and annotated with `@riverpod` (code generation) or equivalent manual provider
WCAG 2.2 AA: `activeTabIndex` changes trigger a screen reader announcement via `SemanticsService.announce` (or equivalent) — this must be called from the UI layer observing the provider, not from the notifier itself

Technical Requirements

frameworks
Flutter
Riverpod (riverpod_annotation for code gen)
Dart
apis
NavigationStateRepository (local persistence abstraction, defined in task-002)
data models
TabState
NavigationStackSnapshot
performance requirements
In-memory snapshot reads must be O(1) — index into a fixed-length list
Persist calls are debounced at 300ms to batch rapid tab switches into a single write
Initialization hydration must complete in under 100ms for typical payloads (5 snapshots)
security requirements
Snapshots must not store sensitive user data (names, IDs from Supabase) — store only navigation paths and scroll offsets
Repository write failures must not expose stack traces to the UI — catch and log internally

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Prefer `AsyncNotifier` over `StateNotifier` because initialization requires async work (repository read). This gives idiomatic loading/error state for free. Define `TabState` as a `freezed` or manually-implemented immutable class with a `copyWith` method. Use a `List.unmodifiable` for the snapshots list to prevent accidental mutation.

For the debounce pattern, use a `Timer? _debounceTimer` field in the notifier — cancel and restart on each mutation, flush on dispose. The `NavigationStateRepository` interface should be injected via a Riverpod `Provider` that is overridden in tests with a `FakeNavigationStateRepository`. Avoid calling `GoRouter` from the notifier — the notifier only manages state; GoRouter observes the notifier via a listener in the shell widget.

This separation is critical for testability.

Testing Requirements

Unit tests in `test/navigation/tab_state_manager_test.dart` using `ProviderContainer`: (1) Initialize with a mock repository returning pre-seeded snapshots — assert `state.snapshots` matches. (2) Call `setActiveTab(2)` — assert `state.activeTabIndex == 2` and repository write was called. (3) Call `setActiveTab(5)` — assert `RangeError` is thrown. (4) Call `updateStackSnapshot(1, mockSnapshot)` — assert `state.snapshots[1] == mockSnapshot`.

(5) Call `getSnapshot(3)` — assert returns snapshot at index 3. (6) Simulate repository write failure — assert state was still updated in-memory and error was logged. (7) Rapid-fire 10 `setActiveTab` calls — assert repository write called at most twice (debounce). Target: 90%+ line coverage on notifier class.

Component
Tab State Manager
service medium
Epic Risks (2)
high impact medium prob technical

StatefulShellRoute branch navigator state can interact unexpectedly with GoRouter's imperative navigation (go, push, replace), causing state snapshots to desync from actual route stacks. This could manifest as a user returning to a tab and seeing a different screen than expected, breaking the core motor-fatigue promise.

Mitigation & Contingency

Mitigation: Write integration tests that simulate cross-tab navigation with nested pushes before any UI layer is built. Pin go_router to a tested minor version and review the StatefulShellRoute changelog before upgrading.

Contingency: If branch navigator state consistently desyncs, fall back to a manual stack snapshot strategy using a custom NavigatorObserver that records and replays navigation events independently of StatefulShellRoute internals.

medium impact medium prob technical

Persisted navigation stacks in shared_preferences can become stale or corrupt if route paths are renamed during development, causing app crashes or infinite redirect loops on cold start for users who have an old snapshot.

Mitigation & Contingency

Mitigation: Version the persisted schema with a format key. On app start, validate that all stored route paths exist in the current route config before restoring; silently discard invalid entries rather than crashing.

Contingency: Implement a safe-mode cold start that skips state restoration after a detected crash (via a dirty-launch flag written at startup and cleared on successful first frame), falling back to the default root tab.