high priority low complexity database pending backend specialist Tier 0

Acceptance Criteria

NavigationStateRepository is implemented as a singleton-accessible class with interface methods: saveTabIndex(int index), getTabIndex() → int, saveScrollPosition(String routeName, double offset), getScrollPosition(String routeName) → double?, saveNavigationStack(List<String> stack), getNavigationStack() → List<String>, clearAll()
NavigationState model is a freezed data class with fields: selectedTabIndex (int, default 0), scrollPositions (Map<String, double>), navigationStack (List<String>), lastUpdatedAt (DateTime)
State is persisted to local storage using either Hive or SharedPreferences (not SQLite — SQLite is for server-synced data; navigation state is device-local only) and survives app cold restart
On first launch (no persisted state), getTabIndex() returns 0 (Home tab) and getNavigationStack() returns an empty list
saveScrollPosition stores per-route scroll offsets keyed by route name string; routes not present in the map return null from getScrollPosition()
All repository methods are async (Future<>) and use a single Hive box or SharedPreferences instance initialized during app startup
Repository is exposed via a Riverpod provider (navigationStateRepositoryProvider) so it can be injected into BLoCs and widgets
Repository write operations complete within 50ms on a mid-range device to avoid janking navigation animations

Technical Requirements

frameworks
Flutter
Riverpod
BLoC
Hive or SharedPreferences (flutter_secure_storage if sensitive)
apis
Hive.openBox / SharedPreferences.getInstance
Riverpod Provider / AsyncNotifierProvider
data models
NavigationState
NavigationStateRepository
performance requirements
Read/write operations must complete within 50ms
Repository initialization at app startup must not add more than 100ms to cold start time
Scroll position map must handle up to 50 route entries without performance degradation
security requirements
Navigation state does not contain personally identifiable information — standard Hive box (not encrypted) is sufficient
Tab index and scroll positions must not be synced to Supabase — this is a purely local concern

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Prefer SharedPreferences over Hive for this use case — navigation state is a small set of scalar values and a JSON-serializable map; Hive adds unnecessary complexity. Use jsonEncode/jsonDecode for the scrollPositions map and navigationStack list when storing in SharedPreferences. Define a NavigationStateKeys class with static const string keys to avoid typos. Expose the repository via a Riverpod provider: final navigationStateRepositoryProvider = Provider((ref) => NavigationStateRepository()); Initialize SharedPreferences in main.dart before runApp and pass the instance via the provider override.

The selectedTabIndex must be read and applied by the StatefulShellRoute in the main navigation widget to restore the user's last active tab — this directly supports the app architecture's StatefulShellRoute tab state preservation requirement described in the App Architecture section.

Testing Requirements

Unit tests: mock the Hive box / SharedPreferences and verify each repository method (save + get round-trip for tabIndex, scrollPosition per route, navigationStack); verify that missing keys return defaults; verify clearAll() resets all fields. Integration test: initialize a real Hive instance in a temporary directory, perform save/get operations, restart the repository (re-initialize without clearing), assert persisted values are recovered. Coverage target: 100% of repository methods. Edge cases: null route name (should throw ArgumentError), scroll position for unknown route (returns null, not 0.0), navigationStack with 100 entries (must not cause serialization errors).

Component
Navigation State Repository
data low
Epic Risks (3)
high impact high prob technical

Flutter's ModalBottomSheet and showDialog do not automatically confine VoiceOver or TalkBack focus to the modal's subtree on all platform versions. Background content may remain reachable by screen readers, confusing users and violating WCAG 2.2 criterion 1.3.1.

Mitigation & Contingency

Mitigation: Wrap modal content in an ExcludeSemantics or BlockSemantics widget for background content. Use a Semantics node with liveRegion on the modal container and manually request focus via FocusScope after the modal animation completes. Test on both iOS (VoiceOver) and Android (TalkBack) during widget development.

Contingency: If platform-level focus trapping is unreliable, implement a custom modal wrapper widget that uses a FocusTrap widget (available in Flutter's internal tooling) and an Overlay entry with semantics blocking on the dimmed background layer.

medium impact medium prob technical

On iOS, the system-level swipe-back gesture (UINavigationController) can bypass PopScope and GoRouter's gesture suppression, meaning users can still accidentally dismiss screens via swipe even after the component is implemented. This breaks the gesture-free contract for motor-impaired users.

Mitigation & Contingency

Mitigation: Set popGestureEnabled: false in GoRouter route configurations where swipe-back is suppressed. Test specifically against Flutter's CupertinoPageRoute, which respects this flag, and verify that GoRouter generates Cupertino routes on iOS rather than Material routes with gesture enabled.

Contingency: If go_router's popGestureEnabled flag does not propagate correctly, wrap affected routes in a WillPopScope replacement (PopScope with canPop: false) and file a bug with the go_router maintainers. Document the workaround in the navigation-route-config component for future maintainers.

medium impact medium prob scope

The feature description implies migrating all existing ModalBottomSheet and dialog call sites across the app to use the new accessible helpers, which is a cross-cutting change. Scope underestimation could mean the epic finishes the new components but leaves many call sites un-migrated, leaving the accessibility promise partially broken.

Mitigation & Contingency

Mitigation: Audit all existing modal call sites at the start of the epic (grep for showModalBottomSheet, showDialog, showCupertinoDialog) and add the count to the task list. Treat migration as explicit tasks, not an implied post-step.

Contingency: If migration scope grows beyond the epic's estimate, create a follow-up tech-debt epic scoped only to call-site migration, and gate the release on at minimum all flows used by the accessibility user-story acceptance criteria being migrated.