critical priority low complexity infrastructure pending infrastructure specialist Tier 1

Acceptance Criteria

LocalStorageAdapter class exists with SharedPreferences injected via constructor (required positional or named parameter)
Exposes Future<String?> readString(String key), Future<void> writeString(String key, String value), Future<void> delete(String key)
Exposes Future<int?> readInt(String key), Future<void> writeInt(String key, int value)
Exposes Future<bool?> readBool(String key), Future<void> writeBool(String key, bool value)
Exposes Future<T?> readJson<T>(String key, T Function(Map<String, dynamic>) fromJson) and Future<void> writeJson<T>(String key, T value, Map<String, dynamic> Function(T) toJson)
All read methods return null (not throw) when the key does not exist
All write methods return normally when called with valid arguments; no silent swallowing of SharedPreferences exceptions — exceptions propagate to the caller
delete() is idempotent — calling it on a non-existent key does not throw
readJson throws a FormatException (not a generic exception) if stored JSON is malformed, with a message indicating the key that failed
Class is not a singleton — it is instantiable and injectable
dart analyze reports no warnings or errors on the file

Technical Requirements

frameworks
Flutter
shared_preferences
flutter_test
data models
Organization
performance requirements
All operations are async and non-blocking on the main isolate
readJson must not perform synchronous JSON parsing on large payloads — use compute() if payload exceeds 50KB (document the threshold)
security requirements
Do not log key values or stored data in production builds — wrap any debug logging in assert() or kDebugMode guards
SharedPreferences data is stored in plaintext on device — document in class-level dartdoc that sensitive data must not be stored via this adapter

Execution Context

Execution Tier
Tier 1

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, the generic signature `T? readJson(String key, T Function(Map) fromJson)` follows the pattern used by json_serializable-generated fromJson factories — callers can pass `MyModel.fromJson` directly.

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.

Component
Local Storage Adapter
infrastructure low
Epic Risks (2)
medium impact medium prob technical

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.

high impact low prob dependency

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.