Implement LocalStorageAdapter class
epic-organization-selection-infrastructure-task-002 — Build the LocalStorageAdapter as a typed wrapper around SharedPreferences. Expose async read/write/delete methods for string, int, bool, and JSON-serializable values. Inject SharedPreferences via constructor for testability. Handle null returns and missing keys gracefully with typed Optional/nullable returns.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
Inject SharedPreferences via the constructor rather than calling SharedPreferences.getInstance() internally — this is the key design decision that enables unit testing without platform channels. The Riverpod provider (task-003) will call getInstance() once and inject the result. For readJson
Error handling strategy: SharedPreferences.setString() returns bool in older versions but is void in modern versions — handle both. For delete, use prefs.remove(key) which is already idempotent. Do not add caching inside the adapter — let callers decide whether to cache. Keep the class focused: storage read/write only, no business logic, no key constants (those live in task-001's file).
Testing Requirements
Unit tests using flutter_test must cover: (1) writeString then readString returns the same value; (2) readString on missing key returns null; (3) delete removes the key (subsequent read returns null); (4) writeInt/readInt round-trip; (5) writeBool/readBool round-trip; (6) writeJson/readJson round-trip with a simple model; (7) readJson with malformed JSON throws FormatException; (8) delete on non-existent key does not throw. Use SharedPreferences.setMockInitialValues({}) in setUp() for full test isolation. Achieve 100% branch coverage as specified in the task description. Run with flutter test --coverage and verify lcov output shows 100% on local_storage_adapter.dart.
SharedPreferences behaves differently on iOS (NSUserDefaults) vs Android (SharedPreferences) for edge cases such as first-launch cold reads, storage quota exceeded, or process kill mid-write. If the adapter does not account for these differences, the persistence layer can silently return null on one platform while returning a stale value on the other, causing incorrect routing decisions downstream.
Mitigation & Contingency
Mitigation: Write platform-specific integration tests using flutter_test device runners for both iOS and Android. Document known platform delta in the adapter's inline comments and encode defensive fallback for null returns at the repository boundary.
Contingency: If platform delta causes persistent issues, replace SharedPreferences with flutter_secure_storage for this key — the LocalStorageAdapter abstraction makes this a single-file swap with no impact on the repository or service layer.
The shared_preferences Flutter plugin may have a version conflict with other plugins already in the project pubspec.yaml. A conflict discovered late in the epic blocks all downstream epics.
Mitigation & Contingency
Mitigation: Resolve and pin the shared_preferences version in pubspec.yaml as the very first task of this epic before writing any implementation code. Run flutter pub get and resolve any version conflicts immediately.
Contingency: If version pinning is impossible due to transitive conflicts, implement LocalStorageAdapter using path_provider + dart:io for JSON file storage as an alternative — the interface contract remains unchanged.