2026-04-20 18:39:49 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
|
|
|
|
|
|
import '../models/tracker.dart';
|
|
|
|
|
|
|
|
|
|
/// Persists the list of trackers to [SharedPreferences] as JSON.
|
|
|
|
|
///
|
|
|
|
|
/// A repository is justified here because storage is asynchronous, has
|
|
|
|
|
/// specialized queries (save/load all), and is consumed both by the service
|
|
|
|
|
/// layer and by tests with fake implementations.
|
|
|
|
|
class TrackerStorage {
|
|
|
|
|
TrackerStorage(this._prefs);
|
|
|
|
|
|
|
|
|
|
static const String _storageKey = 'bible_tracker.trackers.v1';
|
|
|
|
|
|
|
|
|
|
final SharedPreferences _prefs;
|
|
|
|
|
|
2026-04-20 19:13:58 +00:00
|
|
|
/// Synchronous because the underlying [SharedPreferences] getter already
|
|
|
|
|
/// works against an in-memory cache. Keeping this sync lets the service
|
|
|
|
|
/// start up with real data on the first frame, with no async-load race.
|
|
|
|
|
List<Tracker> loadAll() {
|
2026-04-20 18:39:49 +00:00
|
|
|
final String? raw = _prefs.getString(_storageKey);
|
|
|
|
|
if (raw == null || raw.isEmpty) {
|
|
|
|
|
return <Tracker>[];
|
|
|
|
|
}
|
|
|
|
|
final List<dynamic> decoded = jsonDecode(raw) as List<dynamic>;
|
|
|
|
|
return decoded
|
|
|
|
|
.cast<Map<String, dynamic>>()
|
|
|
|
|
.map(Tracker.fromJson)
|
|
|
|
|
.toList(growable: false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> saveAll(List<Tracker> trackers) async {
|
|
|
|
|
final List<Map<String, dynamic>> json = trackers
|
|
|
|
|
.map((Tracker tracker) => tracker.toJson())
|
|
|
|
|
.toList(growable: false);
|
|
|
|
|
await _prefs.setString(_storageKey, jsonEncode(json));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Must be overridden at startup with an initialized [SharedPreferences].
|
|
|
|
|
final Provider<SharedPreferences> sharedPreferencesProvider =
|
|
|
|
|
Provider<SharedPreferences>(
|
|
|
|
|
(Ref ref) => throw UnimplementedError(
|
|
|
|
|
'sharedPreferencesProvider must be overridden before use',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final Provider<TrackerStorage> trackerStorageProvider =
|
|
|
|
|
Provider<TrackerStorage>(
|
|
|
|
|
(Ref ref) => TrackerStorage(ref.read(sharedPreferencesProvider)),
|
|
|
|
|
);
|