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:
John Mizerek 2026-02-21 09:22:48 -08:00
parent 615e18d03a
commit 40275bcd0c
9 changed files with 600 additions and 176 deletions

View file

@ -1,12 +1,20 @@
import 'enums.dart';
class BehavioralInputs {
// Screen 1: Basic behaviors
final SmokingStatus smoking;
final int cigarettesPerDay; // only relevant if smoking == current
final int cigarettesPerDay;
final AlcoholLevel alcohol;
final double sleepHours;
final bool sleepConsistent;
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 WorkHoursLevel workHours;
@ -17,6 +25,11 @@ class BehavioralInputs {
required this.sleepHours,
required this.sleepConsistent,
required this.activity,
required this.diet,
required this.processedFood,
required this.drugUse,
required this.social,
required this.stress,
required this.driving,
required this.workHours,
});
@ -28,10 +41,41 @@ class BehavioralInputs {
sleepHours: 7.5,
sleepConsistent: true,
activity: ActivityLevel.high,
diet: DietQuality.excellent,
processedFood: ProcessedFoodLevel.rarely,
drugUse: DrugUse.none,
social: SocialConnection.strong,
stress: StressLevel.low,
driving: DrivingExposure.low,
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() => {
'smoking': smoking.name,
'cigarettesPerDay': cigarettesPerDay,
@ -39,6 +83,11 @@ class BehavioralInputs {
'sleepHours': sleepHours,
'sleepConsistent': sleepConsistent,
'activity': activity.name,
'diet': diet.name,
'processedFood': processedFood.name,
'drugUse': drugUse.name,
'social': social.name,
'stress': stress.name,
'driving': driving.name,
'workHours': workHours.name,
};
@ -51,6 +100,14 @@ class BehavioralInputs {
sleepHours: (json['sleepHours'] as num).toDouble(),
sleepConsistent: json['sleepConsistent'] as bool,
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),
workHours: WorkHoursLevel.values.byName(json['workHours'] as String),
);
@ -62,6 +119,11 @@ class BehavioralInputs {
double? sleepHours,
bool? sleepConsistent,
ActivityLevel? activity,
DietQuality? diet,
ProcessedFoodLevel? processedFood,
DrugUse? drugUse,
SocialConnection? social,
StressLevel? stress,
DrivingExposure? driving,
WorkHoursLevel? workHours,
}) =>
@ -72,6 +134,11 @@ class BehavioralInputs {
sleepHours: sleepHours ?? this.sleepHours,
sleepConsistent: sleepConsistent ?? this.sleepConsistent,
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,
workHours: workHours ?? this.workHours,
);

View file

@ -13,3 +13,14 @@ enum WorkHoursLevel { normal, elevated, high, extreme }
enum Diagnosis { cardiovascular, diabetes, cancer, copd, hypertension }
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 }

View file

@ -3,7 +3,7 @@ import '../models/models.dart';
import 'hazard_ratios.dart';
import 'mortality_tables.dart';
const String modelVersion = '1.0';
const String modelVersion = '1.1';
/// Maximum combined hazard ratio (prevents unrealistic compounding).
const double _maxCombinedHR = 4.0;
@ -19,11 +19,19 @@ const double _highMultiplier = 1.2;
double computeCombinedHazard(BehavioralInputs inputs, double bmi) {
double hr = 1.0;
// Screen 1 factors
hr *= getSmokingHR(inputs.smoking, inputs.cigarettesPerDay);
hr *= getAlcoholHR(inputs.alcohol);
hr *= getSleepHR(inputs.sleepHours, inputs.sleepConsistent);
hr *= getActivityHR(inputs.activity);
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 *= getWorkHoursHR(inputs.workHours);
@ -38,7 +46,6 @@ LifespanDelta _computeDelta(
String behaviorKey,
) {
if (currentHR <= modifiedHR) {
// No improvement possible or already optimal
return LifespanDelta(
lowMonths: 0,
highMonths: 0,
@ -46,11 +53,9 @@ LifespanDelta _computeDelta(
);
}
// Delta years baselineYears × (1 - modifiedHR/currentHR) × dampingFactor
final rawDeltaYears =
baselineYears * (1 - modifiedHR / currentHR) * _dampingFactor;
// Convert to months with uncertainty range
final midpointMonths = rawDeltaYears * 12;
final lowMonths = (midpointMonths * _lowMultiplier).round();
final highMonths = (midpointMonths * _highMultiplier).round();
@ -76,6 +81,16 @@ BehavioralInputs _setToOptimal(BehavioralInputs inputs, String behaviorKey) {
return inputs.copyWith(sleepHours: 7.5, sleepConsistent: true);
case 'activity':
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':
return inputs.copyWith(driving: DrivingExposure.low);
case 'workHours':
@ -99,6 +114,16 @@ bool _isOptimal(BehavioralInputs inputs, String behaviorKey) {
inputs.sleepConsistent;
case 'activity':
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':
return inputs.driving == DrivingExposure.low;
case 'workHours':
@ -114,6 +139,11 @@ const _modifiableBehaviors = [
'alcohol',
'sleep',
'activity',
'diet',
'processedFood',
'drugUse',
'social',
'stress',
'driving',
'workHours',
];
@ -123,28 +153,22 @@ CalculationResult calculateRankedFactors(
UserProfile profile,
BehavioralInputs inputs,
) {
// Get baseline remaining life expectancy
final baselineYears = getRemainingLifeExpectancy(
profile.age,
profile.sex,
profile.country,
);
// Apply existing condition modifiers (reduces baseline)
final conditionHR = getDiagnosisHR(profile.diagnoses);
final adjustedBaselineYears = baselineYears / conditionHR;
// Calculate current combined HR from behaviors
final currentHR = computeCombinedHazard(inputs, profile.bmi);
// Calculate delta for each modifiable behavior
final factors = <RankedFactor>[];
for (final behaviorKey in _modifiableBehaviors) {
// Skip if already optimal
if (_isOptimal(inputs, behaviorKey)) continue;
// Compute HR with this behavior set to optimal
final modifiedInputs = _setToOptimal(inputs, behaviorKey);
final modifiedHR = computeCombinedHazard(modifiedInputs, profile.bmi);
@ -155,7 +179,6 @@ CalculationResult calculateRankedFactors(
behaviorKey,
);
// Only include if there's meaningful gain (> 1 month)
if (delta.highMonths >= 1) {
factors.add(RankedFactor(
behaviorKey: behaviorKey,
@ -165,8 +188,8 @@ CalculationResult calculateRankedFactors(
}
}
// Sort by midpoint delta descending
factors.sort((a, b) => b.delta.midpointMonths.compareTo(a.delta.midpointMonths));
factors.sort(
(a, b) => b.delta.midpointMonths.compareTo(a.delta.midpointMonths));
return CalculationResult(
rankedFactors: factors,

View file

@ -43,7 +43,6 @@ double getSleepHR(double hours, bool consistent) {
hr = 1.10;
}
// Inconsistent sleep schedule adds additional risk
if (!consistent) {
hr *= 1.05;
}
@ -70,8 +69,7 @@ double getBmiHR(double bmi) {
if (bmi >= 30 && bmi < 35) return 1.2;
if (bmi >= 35 && bmi < 40) return 1.4;
if (bmi >= 40) return 1.8;
// Underweight
return 1.15;
return 1.15; // Underweight
}
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.
double getDiagnosisHR(Set<Diagnosis> diagnoses) {
double hr = 1.0;
@ -131,11 +201,16 @@ Confidence getConfidenceForBehavior(String behaviorKey) {
case 'smoking':
case 'alcohol':
case 'activity':
case 'social':
return Confidence.high;
case 'sleep':
case 'workHours':
case 'diet':
case 'stress':
return Confidence.moderate;
case 'driving':
case 'processedFood':
case 'drugUse':
return Confidence.emerging;
default:
return Confidence.moderate;
@ -157,6 +232,16 @@ String getDisplayName(String behaviorKey) {
return 'Driving Exposure';
case 'workHours':
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:
return behaviorKey;
}

View file

@ -3,7 +3,7 @@ import 'package:flutter/services.dart';
import '../models/models.dart';
import '../storage/local_storage.dart';
import '../theme.dart';
import 'results_screen.dart';
import 'lifestyle_screen.dart';
class BehavioralScreen extends StatefulWidget {
final UserProfile profile;
@ -21,8 +21,6 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
double _sleepHours = 7.5;
bool _sleepConsistent = true;
ActivityLevel _activity = ActivityLevel.moderate;
DrivingExposure _driving = DrivingExposure.low;
WorkHoursLevel _workHours = WorkHoursLevel.normal;
@override
void initState() {
@ -40,8 +38,6 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
_sleepHours = behaviors.sleepHours;
_sleepConsistent = behaviors.sleepConsistent;
_activity = behaviors.activity;
_driving = behaviors.driving;
_workHours = behaviors.workHours;
});
}
}
@ -50,7 +46,7 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Behaviors'),
title: const Text('Habits'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
@ -62,12 +58,12 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Modifiable Factors',
'Daily Habits',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
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,
),
const SizedBox(height: 32),
@ -100,26 +96,14 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
_buildSectionLabel('Physical Activity'),
const SizedBox(height: 12),
_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),
// Calculate button
// Continue button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _calculate,
child: const Text('Calculate'),
onPressed: _continue,
child: const Text('Continue'),
),
),
const SizedBox(height: 16),
@ -166,7 +150,6 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
max: 40,
divisions: 39,
onChanged: (value) {
// Haptic feedback at 20 (one pack)
if (value.round() == 20 && _cigarettesPerDay != 20) {
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>({
required T value,
required List<(T, String)> options,
@ -329,29 +286,19 @@ class _BehavioralScreenState extends State<BehavioralScreen> {
);
}
void _calculate() async {
final behaviors = BehavioralInputs(
void _continue() {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => LifestyleScreen(
profile: widget.profile,
smoking: _smoking,
cigarettesPerDay: _cigarettesPerDay,
alcohol: _alcohol,
sleepHours: _sleepHours,
sleepConsistent: _sleepConsistent,
activity: _activity,
driving: _driving,
workHours: _workHours,
);
await LocalStorage.saveBehaviors(behaviors);
if (mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ResultsScreen(
profile: widget.profile,
behaviors: behaviors,
),
),
);
}
}
}

View 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,
),
),
);
}
}
}

View file

@ -2,4 +2,5 @@ export 'onboarding_screen.dart';
export 'welcome_screen.dart';
export 'baseline_screen.dart';
export 'behavioral_screen.dart';
export 'lifestyle_screen.dart';
export 'results_screen.dart';

View file

@ -13,60 +13,44 @@ class WelcomeScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
children: [
const Spacer(flex: 2),
// Icon or logo area
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
const Spacer(flex: 1),
// Main question - large and centered
Text(
'Add Months',
style: Theme.of(context).textTheme.headlineLarge,
'Simple questions.\nHonest answers.',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontSize: 32,
height: 1.3,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Description
const SizedBox(height: 40),
Text(
'Identify the single change most likely to extend your lifespan.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
"What's the single biggest change I can make to live a longer, healthier life?",
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,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
// Features list
_buildFeatureItem(
context,
Icons.shield_outlined,
'Private',
const SizedBox(width: 8),
Text(
'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),
_buildFeatureItem(
context,
Icons.trending_up_outlined,
'Actionable',
'Focus on what matters most',
),
const Spacer(flex: 3),
const SizedBox(height: 24),
// Start button
SizedBox(
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) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const BaselineScreen()),

View file

@ -94,6 +94,11 @@ void main() {
sleepHours: 5.0,
sleepConsistent: false,
activity: ActivityLevel.sedentary,
diet: DietQuality.poor,
processedFood: ProcessedFoodLevel.daily,
drugUse: DrugUse.regular,
social: SocialConnection.isolated,
stress: StressLevel.chronic,
driving: DrivingExposure.veryHigh,
workHours: WorkHoursLevel.extreme,
);
@ -121,6 +126,11 @@ void main() {
sleepHours: 7.5,
sleepConsistent: true,
activity: ActivityLevel.high,
diet: DietQuality.excellent,
processedFood: ProcessedFoodLevel.rarely,
drugUse: DrugUse.none,
social: SocialConnection.strong,
stress: StressLevel.low,
driving: DrivingExposure.veryHigh,
workHours: WorkHoursLevel.normal,
);
@ -138,7 +148,7 @@ void main() {
test('result includes model version', () {
final result = calculateRankedFactors(healthyProfile, unhealthyInputs);
expect(result.modelVersion, equals('1.0'));
expect(result.modelVersion, equals('1.1'));
});
test('sedentary person sees activity as factor', () {
@ -149,6 +159,11 @@ void main() {
sleepHours: 7.5,
sleepConsistent: true,
activity: ActivityLevel.sedentary,
diet: DietQuality.excellent,
processedFood: ProcessedFoodLevel.rarely,
drugUse: DrugUse.none,
social: SocialConnection.strong,
stress: StressLevel.low,
driving: DrivingExposure.low,
workHours: WorkHoursLevel.normal,
);
@ -205,6 +220,11 @@ void main() {
sleepHours: 7.5,
sleepConsistent: true,
activity: ActivityLevel.sedentary,
diet: DietQuality.excellent,
processedFood: ProcessedFoodLevel.rarely,
drugUse: DrugUse.none,
social: SocialConnection.strong,
stress: StressLevel.low,
driving: DrivingExposure.low,
workHours: WorkHoursLevel.normal,
);