diff --git a/lib/blocs/app_bloc.dart b/lib/blocs/app_bloc.dart index 9bb9f778..889e8fad 100644 --- a/lib/blocs/app_bloc.dart +++ b/lib/blocs/app_bloc.dart @@ -155,6 +155,12 @@ class StudyAppBLoC { // set up and initialize sensing await Sensing().initialize(); + // add the study and configure sensing + await Sensing().addStudy(); + + // initialize the UI data models + data.init(Sensing().controller!); + // set up the messaging part messageManager.initialize().then( (value) { @@ -164,12 +170,6 @@ class StudyAppBLoC { }, ); - // add the study and configure sensing - await Sensing().addStudy(); - - // initialize the UI data models - data.init(Sensing().controller!); - debug('$runtimeType configuration done.'); _state = StudyAppState.configured; } diff --git a/lib/main.g.dart b/lib/main.g.dart index 28a09def..75aaca78 100644 --- a/lib/main.g.dart +++ b/lib/main.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of carp_study_app; +part of 'main.dart'; // ************************************************************************** // JsonSerializableGenerator diff --git a/lib/ui/cards/heart_rate_card.dart b/lib/ui/cards/heart_rate_card.dart index e2ee7554..b56ee566 100644 --- a/lib/ui/cards/heart_rate_card.dart +++ b/lib/ui/cards/heart_rate_card.dart @@ -61,9 +61,9 @@ class HeartRateCardWidgetState extends State StreamBuilder( stream: widget.model.heartRateEvents, builder: (context, AsyncSnapshot snapshot) { - animationController.duration = Duration( - milliseconds: 1000 ~/ - (((widget.model.currentHeartRate ?? 0) + 1) / 60)); + // animationController.duration = Duration( + // milliseconds: 1000 ~/ + // (((widget.model.currentHeartRate ?? 0) + 1) / 60)); return Column( children: [ @@ -177,12 +177,12 @@ class HeartRateCardWidgetState extends State RepaintBoundary( child: ScaleTransition( // scale should be _animation if the isOnWrist is true otherwise it should be no scale - scale: !widget.model.contactStatus + scale: widget.model.contactStatus ? Tween(begin: 1, end: 1) .animate(animationController) : animation, child: Icon( - !widget.model.contactStatus + widget.model.contactStatus ? Icons.favorite_outline_rounded : Icons.favorite_rounded, color: HeartRateCardWidget.colors[0], diff --git a/lib/ui/pages/data_visualization_page.dart b/lib/ui/pages/data_visualization_page.dart index 5bb069b1..ae57a800 100644 --- a/lib/ui/pages/data_visualization_page.dart +++ b/lib/ui/pages/data_visualization_page.dart @@ -12,46 +12,56 @@ class DataVisualizationPage extends StatelessWidget { return Scaffold( backgroundColor: Theme.of(context).colorScheme.secondary, body: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CarpAppBar(), - Container( - color: Theme.of(context).colorScheme.secondary, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: Align( - alignment: Alignment.centerLeft, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${locale.translate('pages.data_viz.hello')} ${bloc.friendlyUsername}' - .toUpperCase(), - style: dataCardTitleStyle.copyWith( - color: Theme.of(context).primaryColor), + child: FutureBuilder( + future: bloc.data._dataVisualizationPageViewModel + .init(Sensing().controller!), + builder: (context, data) { + if (!data.hasData) { + return const Center(child: CircularProgressIndicator()); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CarpAppBar(), + Container( + color: Theme.of(context).colorScheme.secondary, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Align( + alignment: Alignment.centerLeft, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${locale.translate('pages.data_viz.hello')} ${bloc.friendlyUsername}' + .toUpperCase(), + style: dataCardTitleStyle.copyWith( + color: Theme.of(context).primaryColor), + ), + Text(locale.translate('pages.data_viz.thanks'), + style: aboutCardSubtitleStyle), + const SizedBox(height: 15), + ], + ), ), - Text(locale.translate('pages.data_viz.thanks'), - style: aboutCardSubtitleStyle), - const SizedBox(height: 15), - ], + ), ), - ), - ), - ), - Expanded( - flex: 4, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: _dataVizCards, - ), - ), - ) - ], + Expanded( + flex: 4, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: _dataVizCards, + ), + ), + ) + ], + ); + } + }, ), ), ); diff --git a/lib/view_models/cards/activity_data_model.dart b/lib/view_models/cards/activity_data_model.dart index 47c641de..fa2f786e 100644 --- a/lib/view_models/cards/activity_data_model.dart +++ b/lib/view_models/cards/activity_data_model.dart @@ -17,8 +17,8 @@ class ActivityCardViewModel extends SerializableViewModel { .where((measurement) => measurement.data is Activity); @override - void init(SmartphoneDeploymentController ctrl) { - super.init(ctrl); + Future init(SmartphoneDeploymentController ctrl) async { + await super.init(ctrl); // listen for activity events and count the minutes activityEvents?.listen((measurement) { diff --git a/lib/view_models/cards/heart_rate_data_model.dart b/lib/view_models/cards/heart_rate_data_model.dart index c480211e..982740e5 100644 --- a/lib/view_models/cards/heart_rate_data_model.dart +++ b/lib/view_models/cards/heart_rate_data_model.dart @@ -23,8 +23,8 @@ class HeartRateCardViewModel extends SerializableViewModel { .where((measurement) => measurement.data is PolarHR); @override - void init(SmartphoneDeploymentController ctrl) { - super.init(ctrl); + Future init(SmartphoneDeploymentController ctrl) async { + await super.init(ctrl); heartRateEvents?.listen( (heartRateDataPoint) { diff --git a/lib/view_models/cards/mobility_data_model.dart b/lib/view_models/cards/mobility_data_model.dart index 5a2fbb4d..7e5b0eb4 100644 --- a/lib/view_models/cards/mobility_data_model.dart +++ b/lib/view_models/cards/mobility_data_model.dart @@ -12,8 +12,8 @@ class MobilityCardViewModel extends SerializableViewModel { MobilityCardViewModel(); @override - void init(SmartphoneDeploymentController ctrl) { - super.init(ctrl); + Future init(SmartphoneDeploymentController ctrl) async { + await super.init(ctrl); // listen for mobility events and update the features mobilityEvents?.listen((measurement) { diff --git a/lib/view_models/cards/steps_data_model.dart b/lib/view_models/cards/steps_data_model.dart index 9baacef1..c7c0ca37 100644 --- a/lib/view_models/cards/steps_data_model.dart +++ b/lib/view_models/cards/steps_data_model.dart @@ -17,8 +17,8 @@ class StepsCardViewModel extends SerializableViewModel { .where((dataPoint) => dataPoint.data is StepCount); @override - void init(SmartphoneDeploymentController ctrl) { - super.init(ctrl); + Future init(SmartphoneDeploymentController ctrl) async { + await super.init(ctrl); // listen for pedometer events and count them pedometerEvents?.listen((pedometerDataPoint) { diff --git a/lib/view_models/data_visualization_page_model.dart b/lib/view_models/data_visualization_page_model.dart index 6f0e5d38..8dc9a34d 100644 --- a/lib/view_models/data_visualization_page_model.dart +++ b/lib/view_models/data_visualization_page_model.dart @@ -49,18 +49,19 @@ class DataVisualizationPageViewModel extends ViewModel { DataVisualizationPageViewModel(); @override - void init(SmartphoneDeploymentController ctrl) { + Future init(SmartphoneDeploymentController ctrl) async { super.init(ctrl); - _activityCardDataModel.init(ctrl); - _stepsCardDataModel.init(ctrl); + await _activityCardDataModel.init(ctrl); + await _stepsCardDataModel.init(ctrl); _measuresCardDataModel.init(ctrl); - _mobilityCardDataModel.init(ctrl); + await _mobilityCardDataModel.init(ctrl); _surveysCardDataModel.init(ctrl); _audioCardDataModel.init(ctrl); _videoCardDataModel.init(ctrl); _imageCardDataModel.init(ctrl); _studyProgressCardDataModel.init(ctrl); - _heartRateCardDataModel.init(ctrl); + await _heartRateCardDataModel.init(ctrl); + return 1; } /// Delete all persistent view models. diff --git a/lib/view_models/view_model.dart b/lib/view_models/view_model.dart index b4b7115d..86e769b7 100644 --- a/lib/view_models/view_model.dart +++ b/lib/view_models/view_model.dart @@ -43,12 +43,12 @@ abstract class SerializableViewModel extends ViewModel { @override @mustCallSuper - void init(SmartphoneDeploymentController ctrl) { + Future init(SmartphoneDeploymentController ctrl) async { super.init(ctrl); _model = createModel(); // restore the data model (if any saved) - restore().then((savedModel) => _model = savedModel ?? _model); + await restore().then((savedModel) => _model = savedModel ?? _model); // save the data model on a regular basis. Timer.periodic(const Duration(minutes: 3), (_) => save(model.toJson())); @@ -161,9 +161,8 @@ class CarpStudyAppViewModel extends ViewModel { _informedConsentViewModel; @override - void init(SmartphoneDeploymentController ctrl) { + void init(SmartphoneDeploymentController ctrl) async { super.init(ctrl); - _dataVisualizationPageViewModel.init(ctrl); _studyPageViewModel.init(ctrl); _taskListPageViewModel.init(ctrl); _profilePageViewModel.init(ctrl); diff --git a/test/heart_rate_data_model_test.dart b/test/heart_rate_data_model_test.dart index a0ec6174..154f45a4 100644 --- a/test/heart_rate_data_model_test.dart +++ b/test/heart_rate_data_model_test.dart @@ -36,20 +36,21 @@ void main() { setUp(() { when(mockSmartphoneDeploymentController.measurements) .thenAnswer((_) => heartRateStreamController.stream); + + viewModel.init(mockSmartphoneDeploymentController); }); tearDownAll(() { heartRateStreamController.close(); }); group('and update model variables', () { test('with one event', () async { - viewModel.init(mockSmartphoneDeploymentController); // Add a heart rate data point to the stream when(mockPolarHRDatum.hr).thenReturn(80); when(mockMeasurement.data).thenReturn(mockPolarHRDatum); heartRateStreamController.sink.add(mockMeasurement); - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(milliseconds: 100)); expect(viewModel.currentHeartRate, equals(80.0)); expect(viewModel.dayMinMax, equals(HeartRateMinMaxPrHour(80, 80))); expect( @@ -58,20 +59,19 @@ void main() { .hourlyHeartRate)); }); test('with multiple events', () async { - viewModel.init(mockSmartphoneDeploymentController); // Add a heart rate data point to the stream when(mockPolarHRDatum.hr).thenReturn(90); when(mockMeasurement.data).thenReturn(mockPolarHRDatum); heartRateStreamController.sink.add(mockMeasurement); - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(milliseconds: 100)); when(mockPolarHRDatum.hr).thenReturn(60); when(mockMeasurement.data).thenReturn(mockPolarHRDatum); heartRateStreamController.sink.add(mockMeasurement); - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(milliseconds: 100)); expect(viewModel.currentHeartRate, equals(60)); expect(viewModel.dayMinMax, equals(HeartRateMinMaxPrHour(60, 90))); expect( @@ -82,14 +82,13 @@ void main() { .hourlyHeartRate)); }); test('with events with data that is 0', () async { - viewModel.init(mockSmartphoneDeploymentController); // Add a heart rate data point to the stream when(mockPolarHRDatum.hr).thenReturn(0); when(mockMeasurement.data).thenReturn(mockPolarHRDatum); heartRateStreamController.sink.add(mockMeasurement); - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(milliseconds: 100)); expect(viewModel.currentHeartRate, equals(null)); expect( viewModel.dayMinMax, equals(HeartRateMinMaxPrHour(null, null))); @@ -98,7 +97,6 @@ void main() { expect(viewModel.contactStatus, equals(false)); }); test('with contactStatus being true', () async { - viewModel.init(mockSmartphoneDeploymentController); // Add a heart rate data point to the stream when(mockPolarHRDatum.hr).thenReturn(1); when(mockPolarHRDatum.contactStatusSupported).thenReturn(true); @@ -107,7 +105,7 @@ void main() { heartRateStreamController.sink.add(mockMeasurement); - await Future.delayed(const Duration(seconds: 1)); + await Future.delayed(const Duration(milliseconds: 100)); expect(viewModel.currentHeartRate, equals(anything)); expect(viewModel.dayMinMax, equals(anything)); expect(viewModel.hourlyHeartRate, equals(anything)); diff --git a/test/heart_rate_data_model_test.mocks.dart b/test/heart_rate_data_model_test.mocks.dart index 94ccd111..50276a19 100644 --- a/test/heart_rate_data_model_test.mocks.dart +++ b/test/heart_rate_data_model_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.2 from annotations // in carp_study_app/test/heart_rate_data_model_test.dart. // Do not manually edit this file. @@ -503,13 +503,15 @@ class MockHeartRateCardViewModel extends _i1.Mock ), ) as _i4.HourlyHeartRate); @override - void init(_i2.SmartphoneDeploymentController? ctrl) => super.noSuchMethod( + _i5.Future init(_i2.SmartphoneDeploymentController? ctrl) => + (super.noSuchMethod( Invocation.method( #init, [ctrl], ), - returnValueForMissingStub: null, - ); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future save(Map? json) => (super.noSuchMethod( Invocation.method(