Add lifestyle factors screen with diet, social, stress tracking
- Split behavioral inputs into two screens (Habits + Lifestyle) - Added 5 new modifiable factors: diet quality, processed food, drug use, social connection, and stress level - Updated hazard ratios for all new factors based on meta-analyses - Model version bumped to 1.1 - Simplified welcome screen with clearer value proposition - Updated tests for expanded behavioral model Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
615e18d03a
commit
40275bcd0c
9 changed files with 600 additions and 176 deletions
|
|
@ -1,12 +1,20 @@
|
||||||
import 'enums.dart';
|
import 'enums.dart';
|
||||||
|
|
||||||
class BehavioralInputs {
|
class BehavioralInputs {
|
||||||
|
// Screen 1: Basic behaviors
|
||||||
final SmokingStatus smoking;
|
final SmokingStatus smoking;
|
||||||
final int cigarettesPerDay; // only relevant if smoking == current
|
final int cigarettesPerDay;
|
||||||
final AlcoholLevel alcohol;
|
final AlcoholLevel alcohol;
|
||||||
final double sleepHours;
|
final double sleepHours;
|
||||||
final bool sleepConsistent;
|
final bool sleepConsistent;
|
||||||
final ActivityLevel activity;
|
final ActivityLevel activity;
|
||||||
|
|
||||||
|
// Screen 2: Lifestyle factors
|
||||||
|
final DietQuality diet;
|
||||||
|
final ProcessedFoodLevel processedFood;
|
||||||
|
final DrugUse drugUse;
|
||||||
|
final SocialConnection social;
|
||||||
|
final StressLevel stress;
|
||||||
final DrivingExposure driving;
|
final DrivingExposure driving;
|
||||||
final WorkHoursLevel workHours;
|
final WorkHoursLevel workHours;
|
||||||
|
|
||||||
|
|
@ -17,6 +25,11 @@ class BehavioralInputs {
|
||||||
required this.sleepHours,
|
required this.sleepHours,
|
||||||
required this.sleepConsistent,
|
required this.sleepConsistent,
|
||||||
required this.activity,
|
required this.activity,
|
||||||
|
required this.diet,
|
||||||
|
required this.processedFood,
|
||||||
|
required this.drugUse,
|
||||||
|
required this.social,
|
||||||
|
required this.stress,
|
||||||
required this.driving,
|
required this.driving,
|
||||||
required this.workHours,
|
required this.workHours,
|
||||||
});
|
});
|
||||||
|
|
@ -28,10 +41,41 @@ class BehavioralInputs {
|
||||||
sleepHours: 7.5,
|
sleepHours: 7.5,
|
||||||
sleepConsistent: true,
|
sleepConsistent: true,
|
||||||
activity: ActivityLevel.high,
|
activity: ActivityLevel.high,
|
||||||
|
diet: DietQuality.excellent,
|
||||||
|
processedFood: ProcessedFoodLevel.rarely,
|
||||||
|
drugUse: DrugUse.none,
|
||||||
|
social: SocialConnection.strong,
|
||||||
|
stress: StressLevel.low,
|
||||||
driving: DrivingExposure.low,
|
driving: DrivingExposure.low,
|
||||||
workHours: WorkHoursLevel.normal,
|
workHours: WorkHoursLevel.normal,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Create partial inputs from screen 1 (with defaults for screen 2)
|
||||||
|
factory BehavioralInputs.fromScreen1({
|
||||||
|
required SmokingStatus smoking,
|
||||||
|
int cigarettesPerDay = 0,
|
||||||
|
required AlcoholLevel alcohol,
|
||||||
|
required double sleepHours,
|
||||||
|
required bool sleepConsistent,
|
||||||
|
required ActivityLevel activity,
|
||||||
|
}) {
|
||||||
|
return BehavioralInputs(
|
||||||
|
smoking: smoking,
|
||||||
|
cigarettesPerDay: cigarettesPerDay,
|
||||||
|
alcohol: alcohol,
|
||||||
|
sleepHours: sleepHours,
|
||||||
|
sleepConsistent: sleepConsistent,
|
||||||
|
activity: activity,
|
||||||
|
diet: DietQuality.fair,
|
||||||
|
processedFood: ProcessedFoodLevel.frequent,
|
||||||
|
drugUse: DrugUse.none,
|
||||||
|
social: SocialConnection.moderate,
|
||||||
|
stress: StressLevel.moderate,
|
||||||
|
driving: DrivingExposure.low,
|
||||||
|
workHours: WorkHoursLevel.normal,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'smoking': smoking.name,
|
'smoking': smoking.name,
|
||||||
'cigarettesPerDay': cigarettesPerDay,
|
'cigarettesPerDay': cigarettesPerDay,
|
||||||
|
|
@ -39,6 +83,11 @@ class BehavioralInputs {
|
||||||
'sleepHours': sleepHours,
|
'sleepHours': sleepHours,
|
||||||
'sleepConsistent': sleepConsistent,
|
'sleepConsistent': sleepConsistent,
|
||||||
'activity': activity.name,
|
'activity': activity.name,
|
||||||
|
'diet': diet.name,
|
||||||
|
'processedFood': processedFood.name,
|
||||||
|
'drugUse': drugUse.name,
|
||||||
|
'social': social.name,
|
||||||
|
'stress': stress.name,
|
||||||
'driving': driving.name,
|
'driving': driving.name,
|
||||||
'workHours': workHours.name,
|
'workHours': workHours.name,
|
||||||
};
|
};
|
||||||
|
|
@ -51,6 +100,14 @@ class BehavioralInputs {
|
||||||
sleepHours: (json['sleepHours'] as num).toDouble(),
|
sleepHours: (json['sleepHours'] as num).toDouble(),
|
||||||
sleepConsistent: json['sleepConsistent'] as bool,
|
sleepConsistent: json['sleepConsistent'] as bool,
|
||||||
activity: ActivityLevel.values.byName(json['activity'] as String),
|
activity: ActivityLevel.values.byName(json['activity'] as String),
|
||||||
|
diet: DietQuality.values.byName(json['diet'] as String? ?? 'fair'),
|
||||||
|
processedFood: ProcessedFoodLevel.values
|
||||||
|
.byName(json['processedFood'] as String? ?? 'frequent'),
|
||||||
|
drugUse: DrugUse.values.byName(json['drugUse'] as String? ?? 'none'),
|
||||||
|
social: SocialConnection.values
|
||||||
|
.byName(json['social'] as String? ?? 'moderate'),
|
||||||
|
stress:
|
||||||
|
StressLevel.values.byName(json['stress'] as String? ?? 'moderate'),
|
||||||
driving: DrivingExposure.values.byName(json['driving'] as String),
|
driving: DrivingExposure.values.byName(json['driving'] as String),
|
||||||
workHours: WorkHoursLevel.values.byName(json['workHours'] as String),
|
workHours: WorkHoursLevel.values.byName(json['workHours'] as String),
|
||||||
);
|
);
|
||||||
|
|
@ -62,6 +119,11 @@ class BehavioralInputs {
|
||||||
double? sleepHours,
|
double? sleepHours,
|
||||||
bool? sleepConsistent,
|
bool? sleepConsistent,
|
||||||
ActivityLevel? activity,
|
ActivityLevel? activity,
|
||||||
|
DietQuality? diet,
|
||||||
|
ProcessedFoodLevel? processedFood,
|
||||||
|
DrugUse? drugUse,
|
||||||
|
SocialConnection? social,
|
||||||
|
StressLevel? stress,
|
||||||
DrivingExposure? driving,
|
DrivingExposure? driving,
|
||||||
WorkHoursLevel? workHours,
|
WorkHoursLevel? workHours,
|
||||||
}) =>
|
}) =>
|
||||||
|
|
@ -72,6 +134,11 @@ class BehavioralInputs {
|
||||||
sleepHours: sleepHours ?? this.sleepHours,
|
sleepHours: sleepHours ?? this.sleepHours,
|
||||||
sleepConsistent: sleepConsistent ?? this.sleepConsistent,
|
sleepConsistent: sleepConsistent ?? this.sleepConsistent,
|
||||||
activity: activity ?? this.activity,
|
activity: activity ?? this.activity,
|
||||||
|
diet: diet ?? this.diet,
|
||||||
|
processedFood: processedFood ?? this.processedFood,
|
||||||
|
drugUse: drugUse ?? this.drugUse,
|
||||||
|
social: social ?? this.social,
|
||||||
|
stress: stress ?? this.stress,
|
||||||
driving: driving ?? this.driving,
|
driving: driving ?? this.driving,
|
||||||
workHours: workHours ?? this.workHours,
|
workHours: workHours ?? this.workHours,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,14 @@ enum WorkHoursLevel { normal, elevated, high, extreme }
|
||||||
enum Diagnosis { cardiovascular, diabetes, cancer, copd, hypertension }
|
enum Diagnosis { cardiovascular, diabetes, cancer, copd, hypertension }
|
||||||
|
|
||||||
enum Confidence { high, moderate, emerging }
|
enum Confidence { high, moderate, emerging }
|
||||||
|
|
||||||
|
// Lifestyle factors
|
||||||
|
enum DietQuality { poor, fair, good, excellent }
|
||||||
|
|
||||||
|
enum ProcessedFoodLevel { daily, frequent, occasional, rarely }
|
||||||
|
|
||||||
|
enum DrugUse { none, occasional, regular, daily }
|
||||||
|
|
||||||
|
enum SocialConnection { isolated, limited, moderate, strong }
|
||||||
|
|
||||||
|
enum StressLevel { low, moderate, high, chronic }
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import '../models/models.dart';
|
||||||
import 'hazard_ratios.dart';
|
import 'hazard_ratios.dart';
|
||||||
import 'mortality_tables.dart';
|
import 'mortality_tables.dart';
|
||||||
|
|
||||||
const String modelVersion = '1.0';
|
const String modelVersion = '1.1';
|
||||||
|
|
||||||
/// Maximum combined hazard ratio (prevents unrealistic compounding).
|
/// Maximum combined hazard ratio (prevents unrealistic compounding).
|
||||||
const double _maxCombinedHR = 4.0;
|
const double _maxCombinedHR = 4.0;
|
||||||
|
|
@ -19,11 +19,19 @@ const double _highMultiplier = 1.2;
|
||||||
double computeCombinedHazard(BehavioralInputs inputs, double bmi) {
|
double computeCombinedHazard(BehavioralInputs inputs, double bmi) {
|
||||||
double hr = 1.0;
|
double hr = 1.0;
|
||||||
|
|
||||||
|
// Screen 1 factors
|
||||||
hr *= getSmokingHR(inputs.smoking, inputs.cigarettesPerDay);
|
hr *= getSmokingHR(inputs.smoking, inputs.cigarettesPerDay);
|
||||||
hr *= getAlcoholHR(inputs.alcohol);
|
hr *= getAlcoholHR(inputs.alcohol);
|
||||||
hr *= getSleepHR(inputs.sleepHours, inputs.sleepConsistent);
|
hr *= getSleepHR(inputs.sleepHours, inputs.sleepConsistent);
|
||||||
hr *= getActivityHR(inputs.activity);
|
hr *= getActivityHR(inputs.activity);
|
||||||
hr *= getBmiHR(bmi);
|
hr *= getBmiHR(bmi);
|
||||||
|
|
||||||
|
// Screen 2 factors
|
||||||
|
hr *= getDietHR(inputs.diet);
|
||||||
|
hr *= getProcessedFoodHR(inputs.processedFood);
|
||||||
|
hr *= getDrugUseHR(inputs.drugUse);
|
||||||
|
hr *= getSocialHR(inputs.social);
|
||||||
|
hr *= getStressHR(inputs.stress);
|
||||||
hr *= getDrivingHR(inputs.driving);
|
hr *= getDrivingHR(inputs.driving);
|
||||||
hr *= getWorkHoursHR(inputs.workHours);
|
hr *= getWorkHoursHR(inputs.workHours);
|
||||||
|
|
||||||
|
|
@ -38,7 +46,6 @@ LifespanDelta _computeDelta(
|
||||||
String behaviorKey,
|
String behaviorKey,
|
||||||
) {
|
) {
|
||||||
if (currentHR <= modifiedHR) {
|
if (currentHR <= modifiedHR) {
|
||||||
// No improvement possible or already optimal
|
|
||||||
return LifespanDelta(
|
return LifespanDelta(
|
||||||
lowMonths: 0,
|
lowMonths: 0,
|
||||||
highMonths: 0,
|
highMonths: 0,
|
||||||
|
|
@ -46,11 +53,9 @@ LifespanDelta _computeDelta(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delta years ≈ baselineYears × (1 - modifiedHR/currentHR) × dampingFactor
|
|
||||||
final rawDeltaYears =
|
final rawDeltaYears =
|
||||||
baselineYears * (1 - modifiedHR / currentHR) * _dampingFactor;
|
baselineYears * (1 - modifiedHR / currentHR) * _dampingFactor;
|
||||||
|
|
||||||
// Convert to months with uncertainty range
|
|
||||||
final midpointMonths = rawDeltaYears * 12;
|
final midpointMonths = rawDeltaYears * 12;
|
||||||
final lowMonths = (midpointMonths * _lowMultiplier).round();
|
final lowMonths = (midpointMonths * _lowMultiplier).round();
|
||||||
final highMonths = (midpointMonths * _highMultiplier).round();
|
final highMonths = (midpointMonths * _highMultiplier).round();
|
||||||
|
|
@ -76,6 +81,16 @@ BehavioralInputs _setToOptimal(BehavioralInputs inputs, String behaviorKey) {
|
||||||
return inputs.copyWith(sleepHours: 7.5, sleepConsistent: true);
|
return inputs.copyWith(sleepHours: 7.5, sleepConsistent: true);
|
||||||
case 'activity':
|
case 'activity':
|
||||||
return inputs.copyWith(activity: ActivityLevel.high);
|
return inputs.copyWith(activity: ActivityLevel.high);
|
||||||
|
case 'diet':
|
||||||
|
return inputs.copyWith(diet: DietQuality.excellent);
|
||||||
|
case 'processedFood':
|
||||||
|
return inputs.copyWith(processedFood: ProcessedFoodLevel.rarely);
|
||||||
|
case 'drugUse':
|
||||||
|
return inputs.copyWith(drugUse: DrugUse.none);
|
||||||
|
case 'social':
|
||||||
|
return inputs.copyWith(social: SocialConnection.strong);
|
||||||
|
case 'stress':
|
||||||
|
return inputs.copyWith(stress: StressLevel.low);
|
||||||
case 'driving':
|
case 'driving':
|
||||||
return inputs.copyWith(driving: DrivingExposure.low);
|
return inputs.copyWith(driving: DrivingExposure.low);
|
||||||
case 'workHours':
|
case 'workHours':
|
||||||
|
|
@ -99,6 +114,16 @@ bool _isOptimal(BehavioralInputs inputs, String behaviorKey) {
|
||||||
inputs.sleepConsistent;
|
inputs.sleepConsistent;
|
||||||
case 'activity':
|
case 'activity':
|
||||||
return inputs.activity == ActivityLevel.high;
|
return inputs.activity == ActivityLevel.high;
|
||||||
|
case 'diet':
|
||||||
|
return inputs.diet == DietQuality.excellent;
|
||||||
|
case 'processedFood':
|
||||||
|
return inputs.processedFood == ProcessedFoodLevel.rarely;
|
||||||
|
case 'drugUse':
|
||||||
|
return inputs.drugUse == DrugUse.none;
|
||||||
|
case 'social':
|
||||||
|
return inputs.social == SocialConnection.strong;
|
||||||
|
case 'stress':
|
||||||
|
return inputs.stress == StressLevel.low;
|
||||||
case 'driving':
|
case 'driving':
|
||||||
return inputs.driving == DrivingExposure.low;
|
return inputs.driving == DrivingExposure.low;
|
||||||
case 'workHours':
|
case 'workHours':
|
||||||
|
|
@ -114,6 +139,11 @@ const _modifiableBehaviors = [
|
||||||
'alcohol',
|
'alcohol',
|
||||||
'sleep',
|
'sleep',
|
||||||
'activity',
|
'activity',
|
||||||
|
'diet',
|
||||||
|
'processedFood',
|
||||||
|
'drugUse',
|
||||||
|
'social',
|
||||||
|
'stress',
|
||||||
'driving',
|
'driving',
|
||||||
'workHours',
|
'workHours',
|
||||||
];
|
];
|
||||||
|
|
@ -123,28 +153,22 @@ CalculationResult calculateRankedFactors(
|
||||||
UserProfile profile,
|
UserProfile profile,
|
||||||
BehavioralInputs inputs,
|
BehavioralInputs inputs,
|
||||||
) {
|
) {
|
||||||
// Get baseline remaining life expectancy
|
|
||||||
final baselineYears = getRemainingLifeExpectancy(
|
final baselineYears = getRemainingLifeExpectancy(
|
||||||
profile.age,
|
profile.age,
|
||||||
profile.sex,
|
profile.sex,
|
||||||
profile.country,
|
profile.country,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply existing condition modifiers (reduces baseline)
|
|
||||||
final conditionHR = getDiagnosisHR(profile.diagnoses);
|
final conditionHR = getDiagnosisHR(profile.diagnoses);
|
||||||
final adjustedBaselineYears = baselineYears / conditionHR;
|
final adjustedBaselineYears = baselineYears / conditionHR;
|
||||||
|
|
||||||
// Calculate current combined HR from behaviors
|
|
||||||
final currentHR = computeCombinedHazard(inputs, profile.bmi);
|
final currentHR = computeCombinedHazard(inputs, profile.bmi);
|
||||||
|
|
||||||
// Calculate delta for each modifiable behavior
|
|
||||||
final factors = <RankedFactor>[];
|
final factors = <RankedFactor>[];
|
||||||
|
|
||||||
for (final behaviorKey in _modifiableBehaviors) {
|
for (final behaviorKey in _modifiableBehaviors) {
|
||||||
// Skip if already optimal
|
|
||||||
if (_isOptimal(inputs, behaviorKey)) continue;
|
if (_isOptimal(inputs, behaviorKey)) continue;
|
||||||
|
|
||||||
// Compute HR with this behavior set to optimal
|
|
||||||
final modifiedInputs = _setToOptimal(inputs, behaviorKey);
|
final modifiedInputs = _setToOptimal(inputs, behaviorKey);
|
||||||
final modifiedHR = computeCombinedHazard(modifiedInputs, profile.bmi);
|
final modifiedHR = computeCombinedHazard(modifiedInputs, profile.bmi);
|
||||||
|
|
||||||
|
|
@ -155,7 +179,6 @@ CalculationResult calculateRankedFactors(
|
||||||
behaviorKey,
|
behaviorKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only include if there's meaningful gain (> 1 month)
|
|
||||||
if (delta.highMonths >= 1) {
|
if (delta.highMonths >= 1) {
|
||||||
factors.add(RankedFactor(
|
factors.add(RankedFactor(
|
||||||
behaviorKey: behaviorKey,
|
behaviorKey: behaviorKey,
|
||||||
|
|
@ -165,8 +188,8 @@ CalculationResult calculateRankedFactors(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by midpoint delta descending
|
factors.sort(
|
||||||
factors.sort((a, b) => b.delta.midpointMonths.compareTo(a.delta.midpointMonths));
|
(a, b) => b.delta.midpointMonths.compareTo(a.delta.midpointMonths));
|
||||||
|
|
||||||
return CalculationResult(
|
return CalculationResult(
|
||||||
rankedFactors: factors,
|
rankedFactors: factors,
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ double getSleepHR(double hours, bool consistent) {
|
||||||
hr = 1.10;
|
hr = 1.10;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inconsistent sleep schedule adds additional risk
|
|
||||||
if (!consistent) {
|
if (!consistent) {
|
||||||
hr *= 1.05;
|
hr *= 1.05;
|
||||||
}
|
}
|
||||||
|
|
@ -70,8 +69,7 @@ double getBmiHR(double bmi) {
|
||||||
if (bmi >= 30 && bmi < 35) return 1.2;
|
if (bmi >= 30 && bmi < 35) return 1.2;
|
||||||
if (bmi >= 35 && bmi < 40) return 1.4;
|
if (bmi >= 35 && bmi < 40) return 1.4;
|
||||||
if (bmi >= 40) return 1.8;
|
if (bmi >= 40) return 1.8;
|
||||||
// Underweight
|
return 1.15; // Underweight
|
||||||
return 1.15;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double getDrivingHR(DrivingExposure level) {
|
double getDrivingHR(DrivingExposure level) {
|
||||||
|
|
@ -100,6 +98,78 @@ double getWorkHoursHR(WorkHoursLevel level) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- New lifestyle factors ---
|
||||||
|
|
||||||
|
/// Diet quality - based on Mediterranean diet studies
|
||||||
|
double getDietHR(DietQuality level) {
|
||||||
|
switch (level) {
|
||||||
|
case DietQuality.excellent:
|
||||||
|
return 1.0;
|
||||||
|
case DietQuality.good:
|
||||||
|
return 1.05;
|
||||||
|
case DietQuality.fair:
|
||||||
|
return 1.15;
|
||||||
|
case DietQuality.poor:
|
||||||
|
return 1.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Processed food consumption - ultra-processed food studies
|
||||||
|
double getProcessedFoodHR(ProcessedFoodLevel level) {
|
||||||
|
switch (level) {
|
||||||
|
case ProcessedFoodLevel.rarely:
|
||||||
|
return 1.0;
|
||||||
|
case ProcessedFoodLevel.occasional:
|
||||||
|
return 1.05;
|
||||||
|
case ProcessedFoodLevel.frequent:
|
||||||
|
return 1.12;
|
||||||
|
case ProcessedFoodLevel.daily:
|
||||||
|
return 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drug use - excluding alcohol/tobacco (cannabis, recreational drugs)
|
||||||
|
double getDrugUseHR(DrugUse level) {
|
||||||
|
switch (level) {
|
||||||
|
case DrugUse.none:
|
||||||
|
return 1.0;
|
||||||
|
case DrugUse.occasional:
|
||||||
|
return 1.05;
|
||||||
|
case DrugUse.regular:
|
||||||
|
return 1.15;
|
||||||
|
case DrugUse.daily:
|
||||||
|
return 1.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Social connection - loneliness/isolation meta-analyses
|
||||||
|
double getSocialHR(SocialConnection level) {
|
||||||
|
switch (level) {
|
||||||
|
case SocialConnection.strong:
|
||||||
|
return 1.0;
|
||||||
|
case SocialConnection.moderate:
|
||||||
|
return 1.05;
|
||||||
|
case SocialConnection.limited:
|
||||||
|
return 1.2;
|
||||||
|
case SocialConnection.isolated:
|
||||||
|
return 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chronic stress - based on allostatic load research
|
||||||
|
double getStressHR(StressLevel level) {
|
||||||
|
switch (level) {
|
||||||
|
case StressLevel.low:
|
||||||
|
return 1.0;
|
||||||
|
case StressLevel.moderate:
|
||||||
|
return 1.05;
|
||||||
|
case StressLevel.high:
|
||||||
|
return 1.15;
|
||||||
|
case StressLevel.chronic:
|
||||||
|
return 1.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Existing conditions modify baseline mortality but are NOT modifiable.
|
/// Existing conditions modify baseline mortality but are NOT modifiable.
|
||||||
double getDiagnosisHR(Set<Diagnosis> diagnoses) {
|
double getDiagnosisHR(Set<Diagnosis> diagnoses) {
|
||||||
double hr = 1.0;
|
double hr = 1.0;
|
||||||
|
|
@ -131,11 +201,16 @@ Confidence getConfidenceForBehavior(String behaviorKey) {
|
||||||
case 'smoking':
|
case 'smoking':
|
||||||
case 'alcohol':
|
case 'alcohol':
|
||||||
case 'activity':
|
case 'activity':
|
||||||
|
case 'social':
|
||||||
return Confidence.high;
|
return Confidence.high;
|
||||||
case 'sleep':
|
case 'sleep':
|
||||||
case 'workHours':
|
case 'workHours':
|
||||||
|
case 'diet':
|
||||||
|
case 'stress':
|
||||||
return Confidence.moderate;
|
return Confidence.moderate;
|
||||||
case 'driving':
|
case 'driving':
|
||||||
|
case 'processedFood':
|
||||||
|
case 'drugUse':
|
||||||
return Confidence.emerging;
|
return Confidence.emerging;
|
||||||
default:
|
default:
|
||||||
return Confidence.moderate;
|
return Confidence.moderate;
|
||||||
|
|
@ -157,6 +232,16 @@ String getDisplayName(String behaviorKey) {
|
||||||
return 'Driving Exposure';
|
return 'Driving Exposure';
|
||||||
case 'workHours':
|
case 'workHours':
|
||||||
return 'Work Hours';
|
return 'Work Hours';
|
||||||
|
case 'diet':
|
||||||
|
return 'Diet Quality';
|
||||||
|
case 'processedFood':
|
||||||
|
return 'Processed Food';
|
||||||
|
case 'drugUse':
|
||||||
|
return 'Drug Use';
|
||||||
|
case 'social':
|
||||||
|
return 'Social Connection';
|
||||||
|
case 'stress':
|
||||||
|
return 'Chronic Stress';
|
||||||
default:
|
default:
|
||||||
return behaviorKey;
|
return behaviorKey;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
|
||||||
import '../models/models.dart';
|
import '../models/models.dart';
|
||||||
import '../storage/local_storage.dart';
|
import '../storage/local_storage.dart';
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
import 'results_screen.dart';
|
import 'lifestyle_screen.dart';
|
||||||
|
|
||||||
class BehavioralScreen extends StatefulWidget {
|
class BehavioralScreen extends StatefulWidget {
|
||||||
final UserProfile profile;
|
final UserProfile profile;
|
||||||
|
|
@ -21,8 +21,6 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
||||||
double _sleepHours = 7.5;
|
double _sleepHours = 7.5;
|
||||||
bool _sleepConsistent = true;
|
bool _sleepConsistent = true;
|
||||||
ActivityLevel _activity = ActivityLevel.moderate;
|
ActivityLevel _activity = ActivityLevel.moderate;
|
||||||
DrivingExposure _driving = DrivingExposure.low;
|
|
||||||
WorkHoursLevel _workHours = WorkHoursLevel.normal;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -40,8 +38,6 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
||||||
_sleepHours = behaviors.sleepHours;
|
_sleepHours = behaviors.sleepHours;
|
||||||
_sleepConsistent = behaviors.sleepConsistent;
|
_sleepConsistent = behaviors.sleepConsistent;
|
||||||
_activity = behaviors.activity;
|
_activity = behaviors.activity;
|
||||||
_driving = behaviors.driving;
|
|
||||||
_workHours = behaviors.workHours;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +46,7 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Behaviors'),
|
title: const Text('Habits'),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
|
|
@ -62,12 +58,12 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Modifiable Factors',
|
'Daily Habits',
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'These behaviors can be changed. We\'ll identify which has the largest impact.',
|
'Substances, sleep, and activity levels.',
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
@ -100,26 +96,14 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
||||||
_buildSectionLabel('Physical Activity'),
|
_buildSectionLabel('Physical Activity'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildActivitySelector(),
|
_buildActivitySelector(),
|
||||||
const SizedBox(height: 28),
|
|
||||||
|
|
||||||
// Driving Exposure
|
|
||||||
_buildSectionLabel('Driving Exposure'),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildDrivingSelector(),
|
|
||||||
const SizedBox(height: 28),
|
|
||||||
|
|
||||||
// Work Hours
|
|
||||||
_buildSectionLabel('Work Hours'),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
_buildWorkHoursSelector(),
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
// Calculate button
|
// Continue button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _calculate,
|
onPressed: _continue,
|
||||||
child: const Text('Calculate'),
|
child: const Text('Continue'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -166,7 +150,6 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
||||||
max: 40,
|
max: 40,
|
||||||
divisions: 39,
|
divisions: 39,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
// Haptic feedback at 20 (one pack)
|
|
||||||
if (value.round() == 20 && _cigarettesPerDay != 20) {
|
if (value.round() == 20 && _cigarettesPerDay != 20) {
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.mediumImpact();
|
||||||
}
|
}
|
||||||
|
|
@ -264,32 +247,6 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDrivingSelector() {
|
|
||||||
return _buildSegmentedControl<DrivingExposure>(
|
|
||||||
value: _driving,
|
|
||||||
options: [
|
|
||||||
(DrivingExposure.low, '<50 mi/wk'),
|
|
||||||
(DrivingExposure.moderate, '50-150'),
|
|
||||||
(DrivingExposure.high, '150-300'),
|
|
||||||
(DrivingExposure.veryHigh, '300+'),
|
|
||||||
],
|
|
||||||
onChanged: (value) => setState(() => _driving = value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildWorkHoursSelector() {
|
|
||||||
return _buildSegmentedControl<WorkHoursLevel>(
|
|
||||||
value: _workHours,
|
|
||||||
options: [
|
|
||||||
(WorkHoursLevel.normal, '<40'),
|
|
||||||
(WorkHoursLevel.elevated, '40-55'),
|
|
||||||
(WorkHoursLevel.high, '55-70'),
|
|
||||||
(WorkHoursLevel.extreme, '70+'),
|
|
||||||
],
|
|
||||||
onChanged: (value) => setState(() => _workHours = value),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSegmentedControl<T>({
|
Widget _buildSegmentedControl<T>({
|
||||||
required T value,
|
required T value,
|
||||||
required List<(T, String)> options,
|
required List<(T, String)> options,
|
||||||
|
|
@ -329,29 +286,19 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _calculate() async {
|
void _continue() {
|
||||||
final behaviors = BehavioralInputs(
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => LifestyleScreen(
|
||||||
|
profile: widget.profile,
|
||||||
smoking: _smoking,
|
smoking: _smoking,
|
||||||
cigarettesPerDay: _cigarettesPerDay,
|
cigarettesPerDay: _cigarettesPerDay,
|
||||||
alcohol: _alcohol,
|
alcohol: _alcohol,
|
||||||
sleepHours: _sleepHours,
|
sleepHours: _sleepHours,
|
||||||
sleepConsistent: _sleepConsistent,
|
sleepConsistent: _sleepConsistent,
|
||||||
activity: _activity,
|
activity: _activity,
|
||||||
driving: _driving,
|
|
||||||
workHours: _workHours,
|
|
||||||
);
|
|
||||||
|
|
||||||
await LocalStorage.saveBehaviors(behaviors);
|
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (_) => ResultsScreen(
|
|
||||||
profile: widget.profile,
|
|
||||||
behaviors: behaviors,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
323
lib/screens/lifestyle_screen.dart
Normal file
323
lib/screens/lifestyle_screen.dart
Normal file
|
|
@ -0,0 +1,323 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../models/models.dart';
|
||||||
|
import '../storage/local_storage.dart';
|
||||||
|
import '../theme.dart';
|
||||||
|
import 'results_screen.dart';
|
||||||
|
|
||||||
|
class LifestyleScreen extends StatefulWidget {
|
||||||
|
final UserProfile profile;
|
||||||
|
final SmokingStatus smoking;
|
||||||
|
final int cigarettesPerDay;
|
||||||
|
final AlcoholLevel alcohol;
|
||||||
|
final double sleepHours;
|
||||||
|
final bool sleepConsistent;
|
||||||
|
final ActivityLevel activity;
|
||||||
|
|
||||||
|
const LifestyleScreen({
|
||||||
|
super.key,
|
||||||
|
required this.profile,
|
||||||
|
required this.smoking,
|
||||||
|
required this.cigarettesPerDay,
|
||||||
|
required this.alcohol,
|
||||||
|
required this.sleepHours,
|
||||||
|
required this.sleepConsistent,
|
||||||
|
required this.activity,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LifestyleScreen> createState() => _LifestyleScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LifestyleScreenState extends State<LifestyleScreen> {
|
||||||
|
DietQuality _diet = DietQuality.fair;
|
||||||
|
ProcessedFoodLevel _processedFood = ProcessedFoodLevel.frequent;
|
||||||
|
DrugUse _drugUse = DrugUse.none;
|
||||||
|
SocialConnection _social = SocialConnection.moderate;
|
||||||
|
StressLevel _stress = StressLevel.moderate;
|
||||||
|
DrivingExposure _driving = DrivingExposure.low;
|
||||||
|
WorkHoursLevel _workHours = WorkHoursLevel.normal;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadExistingBehaviors();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadExistingBehaviors() async {
|
||||||
|
final behaviors = await LocalStorage.getBehaviors();
|
||||||
|
if (behaviors != null) {
|
||||||
|
setState(() {
|
||||||
|
_diet = behaviors.diet;
|
||||||
|
_processedFood = behaviors.processedFood;
|
||||||
|
_drugUse = behaviors.drugUse;
|
||||||
|
_social = behaviors.social;
|
||||||
|
_stress = behaviors.stress;
|
||||||
|
_driving = behaviors.driving;
|
||||||
|
_workHours = behaviors.workHours;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Lifestyle'),
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Lifestyle Factors',
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Diet, social life, stress, and daily exposures.',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// Diet Quality
|
||||||
|
_buildSectionLabel('Diet Quality'),
|
||||||
|
_buildSectionHint('Vegetables, whole foods, variety'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildDietSelector(),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
// Processed Food
|
||||||
|
_buildSectionLabel('Processed Food'),
|
||||||
|
_buildSectionHint('Fast food, packaged snacks, sugary drinks'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildProcessedFoodSelector(),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
// Drug Use
|
||||||
|
_buildSectionLabel('Recreational Drugs'),
|
||||||
|
_buildSectionHint('Cannabis, stimulants, other substances'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildDrugUseSelector(),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
// Social Connection
|
||||||
|
_buildSectionLabel('Social Connection'),
|
||||||
|
_buildSectionHint('Time with friends, family, community'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildSocialSelector(),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
// Stress Level
|
||||||
|
_buildSectionLabel('Stress Level'),
|
||||||
|
_buildSectionHint('Work pressure, anxiety, life demands'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildStressSelector(),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
// Driving Exposure
|
||||||
|
_buildSectionLabel('Driving Exposure'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildDrivingSelector(),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
|
// Work Hours
|
||||||
|
_buildSectionLabel('Work Hours'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildWorkHoursSelector(),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// Calculate button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _calculate,
|
||||||
|
child: const Text('Calculate'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionLabel(String label) {
|
||||||
|
return Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionHint(String hint) {
|
||||||
|
return Text(
|
||||||
|
hint,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDietSelector() {
|
||||||
|
return _buildSegmentedControl<DietQuality>(
|
||||||
|
value: _diet,
|
||||||
|
options: [
|
||||||
|
(DietQuality.poor, 'Poor'),
|
||||||
|
(DietQuality.fair, 'Fair'),
|
||||||
|
(DietQuality.good, 'Good'),
|
||||||
|
(DietQuality.excellent, 'Excellent'),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _diet = value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProcessedFoodSelector() {
|
||||||
|
return _buildSegmentedControl<ProcessedFoodLevel>(
|
||||||
|
value: _processedFood,
|
||||||
|
options: [
|
||||||
|
(ProcessedFoodLevel.daily, 'Daily'),
|
||||||
|
(ProcessedFoodLevel.frequent, 'Often'),
|
||||||
|
(ProcessedFoodLevel.occasional, 'Sometimes'),
|
||||||
|
(ProcessedFoodLevel.rarely, 'Rarely'),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _processedFood = value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDrugUseSelector() {
|
||||||
|
return _buildSegmentedControl<DrugUse>(
|
||||||
|
value: _drugUse,
|
||||||
|
options: [
|
||||||
|
(DrugUse.none, 'None'),
|
||||||
|
(DrugUse.occasional, 'Occasional'),
|
||||||
|
(DrugUse.regular, 'Regular'),
|
||||||
|
(DrugUse.daily, 'Daily'),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _drugUse = value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSocialSelector() {
|
||||||
|
return _buildSegmentedControl<SocialConnection>(
|
||||||
|
value: _social,
|
||||||
|
options: [
|
||||||
|
(SocialConnection.isolated, 'Isolated'),
|
||||||
|
(SocialConnection.limited, 'Limited'),
|
||||||
|
(SocialConnection.moderate, 'Moderate'),
|
||||||
|
(SocialConnection.strong, 'Strong'),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _social = value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStressSelector() {
|
||||||
|
return _buildSegmentedControl<StressLevel>(
|
||||||
|
value: _stress,
|
||||||
|
options: [
|
||||||
|
(StressLevel.low, 'Low'),
|
||||||
|
(StressLevel.moderate, 'Moderate'),
|
||||||
|
(StressLevel.high, 'High'),
|
||||||
|
(StressLevel.chronic, 'Chronic'),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _stress = value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDrivingSelector() {
|
||||||
|
return _buildSegmentedControl<DrivingExposure>(
|
||||||
|
value: _driving,
|
||||||
|
options: [
|
||||||
|
(DrivingExposure.low, '<50 mi/wk'),
|
||||||
|
(DrivingExposure.moderate, '50-150'),
|
||||||
|
(DrivingExposure.high, '150-300'),
|
||||||
|
(DrivingExposure.veryHigh, '300+'),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _driving = value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWorkHoursSelector() {
|
||||||
|
return _buildSegmentedControl<WorkHoursLevel>(
|
||||||
|
value: _workHours,
|
||||||
|
options: [
|
||||||
|
(WorkHoursLevel.normal, '<40'),
|
||||||
|
(WorkHoursLevel.elevated, '40-55'),
|
||||||
|
(WorkHoursLevel.high, '55-70'),
|
||||||
|
(WorkHoursLevel.extreme, '70+'),
|
||||||
|
],
|
||||||
|
onChanged: (value) => setState(() => _workHours = value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSegmentedControl<T>({
|
||||||
|
required T value,
|
||||||
|
required List<(T, String)> options,
|
||||||
|
required ValueChanged<T> onChanged,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.surfaceVariant,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: options.map((option) {
|
||||||
|
final isSelected = value == option.$1;
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onChanged(option.$1),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? AppColors.primary : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
option.$2,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isSelected ? Colors.white : AppColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculate() async {
|
||||||
|
final behaviors = BehavioralInputs(
|
||||||
|
smoking: widget.smoking,
|
||||||
|
cigarettesPerDay: widget.cigarettesPerDay,
|
||||||
|
alcohol: widget.alcohol,
|
||||||
|
sleepHours: widget.sleepHours,
|
||||||
|
sleepConsistent: widget.sleepConsistent,
|
||||||
|
activity: widget.activity,
|
||||||
|
diet: _diet,
|
||||||
|
processedFood: _processedFood,
|
||||||
|
drugUse: _drugUse,
|
||||||
|
social: _social,
|
||||||
|
stress: _stress,
|
||||||
|
driving: _driving,
|
||||||
|
workHours: _workHours,
|
||||||
|
);
|
||||||
|
|
||||||
|
await LocalStorage.saveBehaviors(behaviors);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ResultsScreen(
|
||||||
|
profile: widget.profile,
|
||||||
|
behaviors: behaviors,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,4 +2,5 @@ export 'onboarding_screen.dart';
|
||||||
export 'welcome_screen.dart';
|
export 'welcome_screen.dart';
|
||||||
export 'baseline_screen.dart';
|
export 'baseline_screen.dart';
|
||||||
export 'behavioral_screen.dart';
|
export 'behavioral_screen.dart';
|
||||||
|
export 'lifestyle_screen.dart';
|
||||||
export 'results_screen.dart';
|
export 'results_screen.dart';
|
||||||
|
|
|
||||||
|
|
@ -13,60 +13,44 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Spacer(flex: 2),
|
const Spacer(flex: 1),
|
||||||
// Icon or logo area
|
// Main question - large and centered
|
||||||
Container(
|
|
||||||
width: 80,
|
|
||||||
height: 80,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.surfaceVariant,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.timeline,
|
|
||||||
size: 40,
|
|
||||||
color: AppColors.primary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
// Title
|
|
||||||
Text(
|
Text(
|
||||||
'Add Months',
|
'Simple questions.\nHonest answers.',
|
||||||
style: Theme.of(context).textTheme.headlineLarge,
|
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||||
|
fontSize: 32,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 40),
|
||||||
// Description
|
|
||||||
Text(
|
Text(
|
||||||
'Identify the single change most likely to extend your lifespan.',
|
"What's the single biggest change I can make to live a longer, healthier life?",
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const Spacer(flex: 2),
|
||||||
|
// Privacy note
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.lock_outline,
|
||||||
|
size: 16,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
const SizedBox(width: 8),
|
||||||
),
|
Text(
|
||||||
const SizedBox(height: 48),
|
|
||||||
// Features list
|
|
||||||
_buildFeatureItem(
|
|
||||||
context,
|
|
||||||
Icons.shield_outlined,
|
|
||||||
'Private',
|
|
||||||
'All data stays on your device',
|
'All data stays on your device',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
_buildFeatureItem(
|
|
||||||
context,
|
|
||||||
Icons.science_outlined,
|
|
||||||
'Evidence-based',
|
|
||||||
'Hazard ratios from meta-analyses',
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 24),
|
||||||
_buildFeatureItem(
|
|
||||||
context,
|
|
||||||
Icons.trending_up_outlined,
|
|
||||||
'Actionable',
|
|
||||||
'Focus on what matters most',
|
|
||||||
),
|
|
||||||
const Spacer(flex: 3),
|
|
||||||
// Start button
|
// Start button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|
@ -83,43 +67,6 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFeatureItem(
|
|
||||||
BuildContext context,
|
|
||||||
IconData icon,
|
|
||||||
String title,
|
|
||||||
String description,
|
|
||||||
) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColors.surfaceVariant,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Icon(icon, color: AppColors.primary, size: 22),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
description,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _navigateToBaseline(BuildContext context) {
|
void _navigateToBaseline(BuildContext context) {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(builder: (_) => const BaselineScreen()),
|
MaterialPageRoute(builder: (_) => const BaselineScreen()),
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,11 @@ void main() {
|
||||||
sleepHours: 5.0,
|
sleepHours: 5.0,
|
||||||
sleepConsistent: false,
|
sleepConsistent: false,
|
||||||
activity: ActivityLevel.sedentary,
|
activity: ActivityLevel.sedentary,
|
||||||
|
diet: DietQuality.poor,
|
||||||
|
processedFood: ProcessedFoodLevel.daily,
|
||||||
|
drugUse: DrugUse.regular,
|
||||||
|
social: SocialConnection.isolated,
|
||||||
|
stress: StressLevel.chronic,
|
||||||
driving: DrivingExposure.veryHigh,
|
driving: DrivingExposure.veryHigh,
|
||||||
workHours: WorkHoursLevel.extreme,
|
workHours: WorkHoursLevel.extreme,
|
||||||
);
|
);
|
||||||
|
|
@ -121,6 +126,11 @@ void main() {
|
||||||
sleepHours: 7.5,
|
sleepHours: 7.5,
|
||||||
sleepConsistent: true,
|
sleepConsistent: true,
|
||||||
activity: ActivityLevel.high,
|
activity: ActivityLevel.high,
|
||||||
|
diet: DietQuality.excellent,
|
||||||
|
processedFood: ProcessedFoodLevel.rarely,
|
||||||
|
drugUse: DrugUse.none,
|
||||||
|
social: SocialConnection.strong,
|
||||||
|
stress: StressLevel.low,
|
||||||
driving: DrivingExposure.veryHigh,
|
driving: DrivingExposure.veryHigh,
|
||||||
workHours: WorkHoursLevel.normal,
|
workHours: WorkHoursLevel.normal,
|
||||||
);
|
);
|
||||||
|
|
@ -138,7 +148,7 @@ void main() {
|
||||||
|
|
||||||
test('result includes model version', () {
|
test('result includes model version', () {
|
||||||
final result = calculateRankedFactors(healthyProfile, unhealthyInputs);
|
final result = calculateRankedFactors(healthyProfile, unhealthyInputs);
|
||||||
expect(result.modelVersion, equals('1.0'));
|
expect(result.modelVersion, equals('1.1'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sedentary person sees activity as factor', () {
|
test('sedentary person sees activity as factor', () {
|
||||||
|
|
@ -149,6 +159,11 @@ void main() {
|
||||||
sleepHours: 7.5,
|
sleepHours: 7.5,
|
||||||
sleepConsistent: true,
|
sleepConsistent: true,
|
||||||
activity: ActivityLevel.sedentary,
|
activity: ActivityLevel.sedentary,
|
||||||
|
diet: DietQuality.excellent,
|
||||||
|
processedFood: ProcessedFoodLevel.rarely,
|
||||||
|
drugUse: DrugUse.none,
|
||||||
|
social: SocialConnection.strong,
|
||||||
|
stress: StressLevel.low,
|
||||||
driving: DrivingExposure.low,
|
driving: DrivingExposure.low,
|
||||||
workHours: WorkHoursLevel.normal,
|
workHours: WorkHoursLevel.normal,
|
||||||
);
|
);
|
||||||
|
|
@ -205,6 +220,11 @@ void main() {
|
||||||
sleepHours: 7.5,
|
sleepHours: 7.5,
|
||||||
sleepConsistent: true,
|
sleepConsistent: true,
|
||||||
activity: ActivityLevel.sedentary,
|
activity: ActivityLevel.sedentary,
|
||||||
|
diet: DietQuality.excellent,
|
||||||
|
processedFood: ProcessedFoodLevel.rarely,
|
||||||
|
drugUse: DrugUse.none,
|
||||||
|
social: SocialConnection.strong,
|
||||||
|
stress: StressLevel.low,
|
||||||
driving: DrivingExposure.low,
|
driving: DrivingExposure.low,
|
||||||
workHours: WorkHoursLevel.normal,
|
workHours: WorkHoursLevel.normal,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue