app/lib/storage/local_storage.dart
John Mizerek 151106aa8e Initial commit: Add Months MVP
Local-first Flutter app that identifies the single behavioral change
most likely to extend lifespan using hazard-based modeling.

Features:
- Risk engine with hazard ratios from meta-analyses
- 50 countries mapped to 4 mortality groups
- 6 modifiable factors: smoking, alcohol, sleep, activity, driving, work hours
- SQLite local storage (no cloud, no accounts)
- Muted clinical UI theme
- 23 unit tests for risk engine

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-20 21:25:00 -08:00

125 lines
3.3 KiB
Dart

import 'dart:convert';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import '../models/models.dart';
class LocalStorage {
static const _dbName = 'add_months.db';
static const _tableName = 'user_data';
static const _version = 1;
static Database? _database;
static Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
static Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, _dbName);
return await openDatabase(
path,
version: _version,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE $_tableName (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
)
''');
},
);
}
// Generic key-value operations
static Future<void> _put(String key, Map<String, dynamic> value) async {
final db = await database;
await db.insert(
_tableName,
{
'key': key,
'value': jsonEncode(value),
'updated_at': DateTime.now().millisecondsSinceEpoch,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
static Future<Map<String, dynamic>?> _get(String key) async {
final db = await database;
final results = await db.query(
_tableName,
where: 'key = ?',
whereArgs: [key],
);
if (results.isEmpty) return null;
return jsonDecode(results.first['value'] as String) as Map<String, dynamic>;
}
// Profile operations
static Future<void> saveProfile(UserProfile profile) async {
await _put('profile', profile.toJson());
}
static Future<UserProfile?> getProfile() async {
final json = await _get('profile');
if (json == null) return null;
return UserProfile.fromJson(json);
}
// Behavioral inputs operations
static Future<void> saveBehaviors(BehavioralInputs behaviors) async {
await _put('behaviors', behaviors.toJson());
}
static Future<BehavioralInputs?> getBehaviors() async {
final json = await _get('behaviors');
if (json == null) return null;
return BehavioralInputs.fromJson(json);
}
// Result operations
static Future<void> saveResult(CalculationResult result) async {
await _put('lastResult', result.toJson());
}
static Future<CalculationResult?> getLastResult() async {
final json = await _get('lastResult');
if (json == null) return null;
return CalculationResult.fromJson(json);
}
// Check if user has completed setup
static Future<bool> hasCompletedSetup() async {
final profile = await getProfile();
final behaviors = await getBehaviors();
return profile != null && behaviors != null;
}
// Delete all data
static Future<void> deleteAllData() async {
final db = await database;
await db.delete(_tableName);
}
// Get last updated timestamp
static Future<DateTime?> getLastUpdated() async {
final db = await database;
final results = await db.query(
_tableName,
columns: ['updated_at'],
orderBy: 'updated_at DESC',
limit: 1,
);
if (results.isEmpty) return null;
return DateTime.fromMillisecondsSinceEpoch(
results.first['updated_at'] as int,
);
}
}