From 783bfba64357bc31a1272b4b1c6f9a38a3b9923a Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 19 Sep 2024 21:56:44 +0200 Subject: [PATCH 1/5] Add checks for Health Connect availability on Android when configuring and reading/writing data --- packages/health/lib/src/health_plugin.dart | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 3da6c82b0..3ebb38737 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -27,6 +27,8 @@ class Health { String? _deviceId; final _deviceInfo = DeviceInfoPlugin(); + HealthConnectSdkStatus _healthConnectSdkStatus = + HealthConnectSdkStatus.sdkUnavailable; Health._() { _registerFromJsonFunctions(); @@ -48,11 +50,27 @@ class Health { /// Configure the health plugin. Must be called before using the plugin. Future configure() async { + if (Platform.isAndroid) { + await _checkIfHealthConnectAvailable(); + } + _deviceId = Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; } + Future _checkIfHealthConnectAvailable() async { + if (!Platform.isAndroid) return; + + _healthConnectSdkStatus = await getHealthConnectSdkStatus() ?? + HealthConnectSdkStatus.sdkUnavailable; + + if (_healthConnectSdkStatus == HealthConnectSdkStatus.sdkUnavailable || _healthConnectSdkStatus == HealthConnectSdkStatus.sdkUnavailableProviderUpdateRequired) { + throw UnsupportedError( + 'Health Connect is not available on this device, prompt the user to install it using installHealthConnect.'); + } + } + /// Check if a given data type is available on the platform bool isDataTypeAvailable(HealthDataType dataType) => Platform.isAndroid ? dataTypeKeysAndroid.contains(dataType) @@ -86,6 +104,7 @@ class Health { List types, { List? permissions, }) async { + await _checkIfHealthConnectAvailable(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( "The lists of types and permissions must be of same length."); @@ -111,6 +130,7 @@ class Health { /// NOTE: The app must be completely killed and restarted for the changes to take effect. /// Not implemented on iOS as there is no way to programmatically remove access. Future revokePermissions() async { + await _checkIfHealthConnectAvailable(); try { if (Platform.isIOS) { throw UnsupportedError( @@ -183,6 +203,7 @@ class Health { List types, { List? permissions, }) async { + await _checkIfHealthConnectAvailable(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( 'The length of [types] must be same as that of [permissions].'); @@ -311,6 +332,7 @@ class Health { DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailable(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -386,6 +408,7 @@ class Health { required DateTime startTime, DateTime? endTime, }) async { + await _checkIfHealthConnectAvailable(); endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -421,6 +444,7 @@ class Health { DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailable(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -461,6 +485,7 @@ class Health { DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailable(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -594,6 +619,7 @@ class Health { double? zinc, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailable(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -674,6 +700,7 @@ class Health { required bool isStartOfCycle, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailable(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -804,6 +831,7 @@ class Health { required DateTime endTime, List recordingMethodsToFilter = const [], }) async { + await _checkIfHealthConnectAvailable(); List dataPoints = []; for (var type in types) { @@ -829,6 +857,7 @@ class Health { required List types, required int interval, List recordingMethodsToFilter = const []}) async { + await _checkIfHealthConnectAvailable(); List dataPoints = []; for (var type in types) { @@ -848,6 +877,7 @@ class Health { int activitySegmentDuration = 1, bool includeManualEntry = true, }) async { + await _checkIfHealthConnectAvailable(); List dataPoints = []; final result = await _prepareAggregateQuery( @@ -1095,6 +1125,7 @@ class Health { String? title, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { + await _checkIfHealthConnectAvailable(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { From 09d3cdf741c0efb41a50472c303cba9acb69f2e4 Mon Sep 17 00:00:00 2001 From: Aamir Farooq Date: Thu, 19 Sep 2024 22:30:04 +0200 Subject: [PATCH 2/5] Make naming more clear --- packages/health/lib/src/health_plugin.dart | 37 +++++++++++----------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 3ebb38737..30635670d 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -50,22 +50,21 @@ class Health { /// Configure the health plugin. Must be called before using the plugin. Future configure() async { - if (Platform.isAndroid) { - await _checkIfHealthConnectAvailable(); - } - + await _checkIfHealthConnectAvailableOnAndroid(); _deviceId = Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; } - Future _checkIfHealthConnectAvailable() async { + Future _checkIfHealthConnectAvailableOnAndroid() async { if (!Platform.isAndroid) return; _healthConnectSdkStatus = await getHealthConnectSdkStatus() ?? HealthConnectSdkStatus.sdkUnavailable; - if (_healthConnectSdkStatus == HealthConnectSdkStatus.sdkUnavailable || _healthConnectSdkStatus == HealthConnectSdkStatus.sdkUnavailableProviderUpdateRequired) { + if (_healthConnectSdkStatus == HealthConnectSdkStatus.sdkUnavailable || + _healthConnectSdkStatus == + HealthConnectSdkStatus.sdkUnavailableProviderUpdateRequired) { throw UnsupportedError( 'Health Connect is not available on this device, prompt the user to install it using installHealthConnect.'); } @@ -104,7 +103,7 @@ class Health { List types, { List? permissions, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( "The lists of types and permissions must be of same length."); @@ -130,7 +129,7 @@ class Health { /// NOTE: The app must be completely killed and restarted for the changes to take effect. /// Not implemented on iOS as there is no way to programmatically remove access. Future revokePermissions() async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); try { if (Platform.isIOS) { throw UnsupportedError( @@ -203,7 +202,7 @@ class Health { List types, { List? permissions, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); if (permissions != null && permissions.length != types.length) { throw ArgumentError( 'The length of [types] must be same as that of [permissions].'); @@ -332,7 +331,7 @@ class Health { DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -408,7 +407,7 @@ class Health { required DateTime startTime, DateTime? endTime, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); endTime ??= startTime; if (startTime.isAfter(endTime)) { throw ArgumentError("startTime must be equal or earlier than endTime"); @@ -444,7 +443,7 @@ class Health { DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -485,7 +484,7 @@ class Health { DateTime? endTime, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -619,7 +618,7 @@ class Health { double? zinc, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -700,7 +699,7 @@ class Health { required bool isStartOfCycle, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { @@ -831,7 +830,7 @@ class Health { required DateTime endTime, List recordingMethodsToFilter = const [], }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); List dataPoints = []; for (var type in types) { @@ -857,7 +856,7 @@ class Health { required List types, required int interval, List recordingMethodsToFilter = const []}) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); List dataPoints = []; for (var type in types) { @@ -877,7 +876,7 @@ class Health { int activitySegmentDuration = 1, bool includeManualEntry = true, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); List dataPoints = []; final result = await _prepareAggregateQuery( @@ -1125,7 +1124,7 @@ class Health { String? title, RecordingMethod recordingMethod = RecordingMethod.automatic, }) async { - await _checkIfHealthConnectAvailable(); + await _checkIfHealthConnectAvailableOnAndroid(); if (Platform.isIOS && [RecordingMethod.active, RecordingMethod.unknown] .contains(recordingMethod)) { From ee231bd487f61fde49fd6916a93e5ab3b291fa8b Mon Sep 17 00:00:00 2001 From: bardram Date: Fri, 20 Sep 2024 15:27:40 +0200 Subject: [PATCH 3/5] More gracefull error handling (less trowing) and update to demo app and README --- packages/health/CHANGELOG.md | 4 +- packages/health/README.md | 8 +- packages/health/example/lib/main.dart | 181 +++++++++++---------- packages/health/lib/src/health_plugin.dart | 98 ++++++----- 4 files changed, 159 insertions(+), 132 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 5f60e92d5..7ed8cf6d2 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -12,7 +12,7 @@ * Remove `includeManualEntry` (previously a boolean) from some of the querying methods in favor of `recordingMethodsToFilter`. * For complete details on relevant changes, see the description of PR [#1023](https://github.com/cph-cachet/flutter-plugins/pull/1023) * Add support for all sleep stages across iOS and Android - * Clean up relevant documentation + * Clean up relevant documentation * Remove undocumented sleep stages * **BREAKING** certain sleep stages were removed/combined into other related stages see PR [#1026](https://github.com/cph-cachet/flutter-plugins/pull/1026) for the complete list of changes and a discussion of the motivation in issue [#985](https://github.com/cph-cachet/flutter-plugins/issues/985) * Android: Add support for `OTHER` workout type @@ -20,7 +20,7 @@ * iOS: add support for menstruation flow, PR [#1008](https://github.com/cph-cachet/flutter-plugins/pull/1008) * Android: Add support for heart rate variability, PR [#1009](https://github.com/cph-cachet/flutter-plugins/pull/1009) * iOS: add support for atrial fibrillation burden, PR [#1031](https://github.com/cph-cachet/flutter-plugins/pull/1031) -* Add support for UUIDs in health records for both HealthKit and Health Connect, PR [#1019](https://github.com/cph-cachet/flutter-plugins/pull/1019) +* Add support for UUIDs in health records for both HealthKit and Health Connect, PR [#1019](https://github.com/cph-cachet/flutter-plugins/pull/1019) * Fix an issue when querying workouts, the native code could respond with an activity that is not supported in the Health package, causing an error - this will fallback to `HealthWorkoutActivityType.other` - PR [#1016](https://github.com/cph-cachet/flutter-plugins/pull/1016) * Remove deprecated Android v1 embeddings, PR [#1021](https://github.com/cph-cachet/flutter-plugins/pull/1021) diff --git a/packages/health/README.md b/packages/health/README.md index 3d4cca4c6..76c3396b7 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -1,6 +1,6 @@ # Health -Enables reading and writing health data from/to Apple Health and Health Connect. +Enables reading and writing health data from/to [Apple Health](https://www.apple.com/health/) and [Google Health Connect](https://health.google/health-connect-android/). > **NOTE:** Google has deprecated the Google Fit API. According to the [documentation](https://developers.google.com/fit/android), as of **May 1st 2024** developers cannot sign up for using the API. As such, this package has removed support for Google Fit as of version 11.0.0 and users are urged to upgrade as soon as possible. @@ -17,7 +17,7 @@ The plugin supports: - cleaning up duplicate data points via the `removeDuplicates` method. - removing data of a given type in a selected period of time using the `delete` method. -Note that for Android, the target phone **needs** to have [Health Connect](https://health.google/health-connect-android/) (which is currently in beta) installed and have access to the internet, otherwise this plugin will not work. +Note that for Android, the target phone **needs** to have the [Health Connect](https://play.google.com/store/apps/details?id=com.google.android.apps.healthdata&hl=en) app installed (which is currently in beta) and have access to the internet. See the tables below for supported health and workout data types. @@ -260,8 +260,8 @@ flutter: PlatformException(FlutterHealth, Results are null, Optional(Error Doma Google Health Connect and Apple HealthKit both provide ways to distinguish samples collected "automatically" and manually entered data by the user. -- Android provides an enum with 4 variations: https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/metadata/Metadata#summary -- iOS has a boolean value: https://developer.apple.com/documentation/healthkit/hkmetadatakeywasuserentered +- Android provides an enum with 4 variations: +- iOS has a boolean value: As such, when fetching data you have the option to filter the fetched data by recording method as such: diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index 038b4cf67..db1069ddf 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -80,17 +80,18 @@ class _HealthAppState extends State { : HealthDataAccess.READ_WRITE) .toList(); + @override void initState() { - // configure the health plugin before use. + // configure the health plugin before use and check the Health Connect status Health().configure(); + Health().getHealthConnectSdkStatus(); super.initState(); } /// Install Google Health Connect on this phone. - Future installHealthConnect() async { - await Health().installHealthConnect(); - } + Future installHealthConnect() async => + await Health().installHealthConnect(); /// Authorize, i.e. get permissions to access relevant health data. Future authorize() async { @@ -132,7 +133,8 @@ class _HealthAppState extends State { final status = await Health().getHealthConnectSdkStatus(); setState(() { - _contentHealthConnectStatus = Text('Health Connect Status: $status'); + _contentHealthConnectStatus = + Text('Health Connect Status: ${status?.name.toUpperCase()}'); _state = AppState.HEALTH_CONNECT_STATUS; }); } @@ -143,7 +145,7 @@ class _HealthAppState extends State { // get data within the last 24 hours final now = DateTime.now(); - final yesterday = now.subtract(Duration(hours: 24)); + final yesterday = now.subtract(const Duration(hours: 24)); // Clear old data points _healthDataList.clear(); @@ -186,7 +188,7 @@ class _HealthAppState extends State { /// following data types. Future addData() async { final now = DateTime.now(); - final earlier = now.subtract(Duration(minutes: 20)); + final earlier = now.subtract(const Duration(minutes: 20)); // Add data for supported types // NOTE: These are only the ones supported on Androids new API Health Connect. @@ -289,7 +291,7 @@ class _HealthAppState extends State { success &= await Health().writeWorkoutData( activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL, title: "Random workout name that shows up in Health Connect", - start: now.subtract(Duration(minutes: 15)), + start: now.subtract(const Duration(minutes: 15)), end: now, totalDistance: 2430, totalEnergyBurned: 400, @@ -378,7 +380,7 @@ class _HealthAppState extends State { /// Delete some random health data. Future deleteData() async { final now = DateTime.now(); - final earlier = now.subtract(Duration(hours: 24)); + final earlier = now.subtract(const Duration(hours: 24)); bool success = true; for (HealthDataType type in types) { @@ -459,78 +461,82 @@ class _HealthAppState extends State { appBar: AppBar( title: const Text('Health Example'), ), - body: Container( - child: Column( - children: [ - Wrap( - spacing: 10, - children: [ + body: Column( + children: [ + Wrap( + spacing: 10, + children: [ + if (Platform.isAndroid) TextButton( - onPressed: authorize, - child: Text("Authenticate", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - if (Platform.isAndroid) + onPressed: getHealthConnectSdkStatus, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.blue)), + child: const Text("Check Health Connect Status", + style: TextStyle(color: Colors.white))), + if (Platform.isAndroid && + Health().healthConnectSdkStatus != + HealthConnectSdkStatus.sdkAvailable) + TextButton( + onPressed: installHealthConnect, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.blue)), + child: const Text("Install Health Connect", + style: TextStyle(color: Colors.white))), + if (Platform.isIOS || + Platform.isAndroid && + Health().healthConnectSdkStatus == + HealthConnectSdkStatus.sdkAvailable) + Wrap(spacing: 10, children: [ TextButton( - onPressed: getHealthConnectSdkStatus, - child: Text("Check Health Connect Status", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( + onPressed: authorize, + style: const ButtonStyle( backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: fetchData, - child: Text("Fetch Data", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: addData, - child: Text("Add Data", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: deleteData, - child: Text("Delete Data", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: fetchStepData, - child: Text("Fetch Step Data", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - TextButton( - onPressed: revokeAccess, - child: Text("Revoke Access", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( - backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - if (Platform.isAndroid) + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Authenticate", + style: TextStyle(color: Colors.white))), TextButton( - onPressed: installHealthConnect, - child: Text("Install Health Connect", - style: TextStyle(color: Colors.white)), - style: ButtonStyle( + onPressed: fetchData, + style: const ButtonStyle( backgroundColor: - MaterialStatePropertyAll(Colors.blue))), - ], - ), - Divider(thickness: 3), - if (_state == AppState.DATA_READY) _dataFiltration, - if (_state == AppState.STEPS_READY) _stepsFiltration, - Expanded(child: Center(child: _content)) - ], - ), + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Fetch Data", + style: TextStyle(color: Colors.white))), + TextButton( + onPressed: addData, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Add Data", + style: TextStyle(color: Colors.white))), + TextButton( + onPressed: deleteData, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Delete Data", + style: TextStyle(color: Colors.white))), + TextButton( + onPressed: fetchStepData, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Fetch Step Data", + style: TextStyle(color: Colors.white))), + TextButton( + onPressed: revokeAccess, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.blue)), + child: const Text("Revoke Access", + style: TextStyle(color: Colors.white))), + ]), + ], + ), + const Divider(thickness: 3), + if (_state == AppState.DATA_READY) _dataFiltration, + if (_state == AppState.STEPS_READY) _stepsFiltration, + Expanded(child: Center(child: _content)) + ], ), ), ); @@ -575,7 +581,7 @@ class _HealthAppState extends State { // Add other entries here if needed ], ), - Divider(thickness: 3), + const Divider(thickness: 3), ], ); @@ -610,7 +616,7 @@ class _HealthAppState extends State { // Add other entries here if needed ], ), - Divider(thickness: 3), + const Divider(thickness: 3), ], ); @@ -618,11 +624,11 @@ class _HealthAppState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - padding: EdgeInsets.all(20), - child: CircularProgressIndicator( + padding: const EdgeInsets.all(20), + child: const CircularProgressIndicator( strokeWidth: 10, )), - Text('Revoking permissions...') + const Text('Revoking permissions...') ], ); @@ -635,11 +641,11 @@ class _HealthAppState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Container( - padding: EdgeInsets.all(20), - child: CircularProgressIndicator( + padding: const EdgeInsets.all(20), + child: const CircularProgressIndicator( strokeWidth: 10, )), - Text('Fetching data...') + const Text('Fetching data...') ], ); @@ -687,23 +693,24 @@ class _HealthAppState extends State { Widget _contentNoData = const Text('No Data to show'); - Widget _contentNotFetched = const Column(children: [ + Widget _contentNotFetched = + const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("Press 'Auth' to get permissions to access health data."), const Text("Press 'Fetch Dat' to get health data."), const Text("Press 'Add Data' to add some random health data."), const Text("Press 'Delete Data' to remove some random health data."), - ], mainAxisAlignment: MainAxisAlignment.center); + ]); Widget _authorized = const Text('Authorization granted!'); Widget _authorizationNotGranted = const Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Authorization not given.'), const Text( 'For Google Health Connect please check if you have added the right permissions and services to the manifest file.'), const Text('For Apple Health check your permissions in Apple Health.'), ], - mainAxisAlignment: MainAxisAlignment.center, ); Widget _contentHealthConnectStatus = const Text( diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 30635670d..8868a519b 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -1,8 +1,8 @@ part of '../health.dart'; -/// Main class for the Plugin. This class works as a singleton and should be accessed -/// via `Health()` factory method. The plugin must be configured using the [configure] method -/// before used. +/// Main class for the Plugin. This class works as a singleton and should be +/// accessed via `Health()` factory method. The plugin must be configured using +/// the [configure] method before used. /// /// Overall, the plugin supports: /// @@ -21,6 +21,17 @@ part of '../health.dart'; /// * Writing different types of specialized health data like the [writeWorkoutData], /// [writeBloodPressure], [writeBloodOxygen], [writeAudiogram], [writeMeal], /// [writeMenstruationFlow], [writeInsulinDelivery] methods. +/// +/// On **Android**, this plugin relies on the Google Health Connect (GHC) SDK. +/// Since Health Connect is not installed on SDK level < 34, the plugin has a +/// set of specialized methods to handle GHC: +/// +/// * [getHealthConnectSdkStatus] to check the status of GHC +/// * [isHealthConnectAvailable] to check if GHC is installed on this phone +/// * [installHealthConnect] to direct the user to the app store to install GHC +/// +/// **Note** that you should check the availability of GHC before using any setter +/// or getter methods. Otherwise, the plugin will throw an exception. class Health { static const MethodChannel _channel = MethodChannel('flutter_health'); static final _instance = Health._(); @@ -34,9 +45,12 @@ class Health { _registerFromJsonFunctions(); } - /// Get the singleton [Health] instance. + /// The singleton [Health] instance. factory Health() => _instance; + /// The latest status on availability of Health Connect SDK on this phone. + HealthConnectSdkStatus get healthConnectSdkStatus => _healthConnectSdkStatus; + /// The type of platform of this device. HealthPlatformType get platformType => Platform.isIOS ? HealthPlatformType.appleHealth @@ -50,26 +64,11 @@ class Health { /// Configure the health plugin. Must be called before using the plugin. Future configure() async { - await _checkIfHealthConnectAvailableOnAndroid(); _deviceId = Platform.isAndroid ? (await _deviceInfo.androidInfo).id : (await _deviceInfo.iosInfo).identifierForVendor; } - Future _checkIfHealthConnectAvailableOnAndroid() async { - if (!Platform.isAndroid) return; - - _healthConnectSdkStatus = await getHealthConnectSdkStatus() ?? - HealthConnectSdkStatus.sdkUnavailable; - - if (_healthConnectSdkStatus == HealthConnectSdkStatus.sdkUnavailable || - _healthConnectSdkStatus == - HealthConnectSdkStatus.sdkUnavailableProviderUpdateRequired) { - throw UnsupportedError( - 'Health Connect is not available on this device, prompt the user to install it using installHealthConnect.'); - } - } - /// Check if a given data type is available on the platform bool isDataTypeAvailable(HealthDataType dataType) => Platform.isAndroid ? dataTypeKeysAndroid.contains(dataType) @@ -128,56 +127,77 @@ class Health { /// /// NOTE: The app must be completely killed and restarted for the changes to take effect. /// Not implemented on iOS as there is no way to programmatically remove access. + /// + /// Android only. On iOS this does nothing. Future revokePermissions() async { + if (Platform.isIOS) return; + await _checkIfHealthConnectAvailableOnAndroid(); try { - if (Platform.isIOS) { - throw UnsupportedError( - 'Revoke permissions is not supported on iOS. Please revoke permissions manually in the settings.'); - } await _channel.invokeMethod('revokePermissions'); - return; } catch (e) { debugPrint('$runtimeType - Exception in revokePermissions(): $e'); } } - /// Returns the current status of Health Connect availability. + /// Checks the current status of Health Connect availability. /// /// See this for more info: /// https://developer.android.com/reference/kotlin/androidx/health/connect/client/HealthConnectClient#getSdkStatus(android.content.Context,kotlin.String) /// - /// Android only. + /// Android only. Returns null on iOS or if an error occurs. Future getHealthConnectSdkStatus() async { + if (Platform.isIOS) return null; + try { - if (Platform.isIOS) { - throw UnsupportedError('Health Connect is not available on iOS.'); - } - final int status = - (await _channel.invokeMethod('getHealthConnectSdkStatus'))!; - return HealthConnectSdkStatus.fromNativeValue(status); + final status = + await _channel.invokeMethod('getHealthConnectSdkStatus'); + _healthConnectSdkStatus = status != null + ? HealthConnectSdkStatus.fromNativeValue(status) + : HealthConnectSdkStatus.sdkUnavailable; + + return _healthConnectSdkStatus; } catch (e) { debugPrint('$runtimeType - Exception in getHealthConnectSdkStatus(): $e'); return null; } } - /// Prompt the user to install the Health Connect app via the installed store - /// (most likely Play Store). + /// Is Google Health Connect available on this phone? /// - /// Android only. + /// Android only. Returns always true on iOS. + Future isHealthConnectAvailable() async => !Platform.isAndroid + ? true + : (await getHealthConnectSdkStatus() == + HealthConnectSdkStatus.sdkAvailable); + + /// Prompt the user to install the Google Health Connect app via the + /// installed store (most likely Play Store). + /// + /// Android only. On iOS this does nothing. Future installHealthConnect() async { + if (Platform.isIOS) return; + try { - if (!Platform.isAndroid) { - throw UnsupportedError( - 'installHealthConnect is only available on Android'); - } await _channel.invokeMethod('installHealthConnect'); } catch (e) { debugPrint('$runtimeType - Exception in installHealthConnect(): $e'); } } + /// Checks if Google Health Connect is available and throws an [UnsupportedError] + /// if not. + /// Internal methods used to check availability before any getter or setter methods. + Future _checkIfHealthConnectAvailableOnAndroid() async { + if (!Platform.isAndroid) return; + + if (!(await isHealthConnectAvailable())) { + throw UnsupportedError( + "Google Health Connect is not available on this Android device. " + "You may prompt the user to install it using the 'installHealthConnect' method"); + } + } + /// Requests permissions to access health data [types]. /// /// Returns true if successful, false otherwise. From f0e8bf9ef72af6e4cd7e918fad35a0df57fd5b58 Mon Sep 17 00:00:00 2001 From: bardram Date: Mon, 23 Sep 2024 16:45:28 +0200 Subject: [PATCH 4/5] Type-safe JSON deserialization using carp_serializable v. 2.0 --- packages/health/CHANGELOG.md | 5 + packages/health/LICENSE | 16 +- packages/health/README.md | 5 + packages/health/example/pubspec.yaml | 2 +- packages/health/lib/health.g.dart | 200 +++++++++--------- .../health/lib/src/health_data_point.dart | 2 +- .../health/lib/src/health_value_types.dart | 36 ++-- packages/health/lib/src/workout_summary.dart | 2 +- packages/health/pubspec.yaml | 2 +- 9 files changed, 136 insertions(+), 134 deletions(-) diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md index 7ed8cf6d2..a85a38e57 100644 --- a/packages/health/CHANGELOG.md +++ b/packages/health/CHANGELOG.md @@ -1,3 +1,8 @@ +## 11.1.0 + +* Fix of [#1043](https://github.com/cph-cachet/flutter-plugins/issues/1043) +* Type-safe JSON deserialization using carp_serializable v. 2.0 + ## 11.0.0 * **BREAKING** Remove Google Fit support in the Android code, as well as Google FIt related dependencies and references throughout the documentation diff --git a/packages/health/LICENSE b/packages/health/LICENSE index 7dfb95c95..0edc55d82 100644 --- a/packages/health/LICENSE +++ b/packages/health/LICENSE @@ -1,17 +1,9 @@ MIT License. -Copyright 2019 Copenhagen Center for Health Technology (CACHET) at the Technical University of Denmark (DTU). +Copyright 2020 the Technical University of Denmark (DTU). -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated -documentation files (the ”Software”), to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ”Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED ”AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED ”AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/health/README.md b/packages/health/README.md index 76c3396b7..68ec291bc 100644 --- a/packages/health/README.md +++ b/packages/health/README.md @@ -455,3 +455,8 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/ | WRESTLING | yes | | | | YOGA | yes | yes | | | OTHER | yes | yes | | + +## License + +This software is copyright (c) the [Technical University of Denmark (DTU)](https://www.dtu.dk) and is part of the [Copenhagen Research Platform](https://carp.cachet.dk/). +This software is available 'as-is' under a [MIT license](LICENSE). diff --git a/packages/health/example/pubspec.yaml b/packages/health/example/pubspec.yaml index f23767d27..d472b3f03 100644 --- a/packages/health/example/pubspec.yaml +++ b/packages/health/example/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.2 permission_handler: ^10.2.0 - carp_serializable: ^1.1.0 # polymorphic json serialization + carp_serializable: ^2.0.0 # polymorphic json serialization health: path: ../ diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart index 02c49c115..fa4423446 100644 --- a/packages/health/lib/health.g.dart +++ b/packages/health/lib/health.g.dart @@ -12,36 +12,36 @@ HealthDataPoint _$HealthDataPointFromJson(Map json) => value: HealthValue.fromJson(json['value'] as Map), type: $enumDecode(_$HealthDataTypeEnumMap, json['type']), unit: $enumDecode(_$HealthDataUnitEnumMap, json['unit']), - dateFrom: DateTime.parse(json['date_from'] as String), - dateTo: DateTime.parse(json['date_to'] as String), + dateFrom: DateTime.parse(json['dateFrom'] as String), + dateTo: DateTime.parse(json['dateTo'] as String), sourcePlatform: - $enumDecode(_$HealthPlatformTypeEnumMap, json['source_platform']), - sourceDeviceId: json['source_device_id'] as String, - sourceId: json['source_id'] as String, - sourceName: json['source_name'] as String, + $enumDecode(_$HealthPlatformTypeEnumMap, json['sourcePlatform']), + sourceDeviceId: json['sourceDeviceId'] as String, + sourceId: json['sourceId'] as String, + sourceName: json['sourceName'] as String, recordingMethod: $enumDecodeNullable( - _$RecordingMethodEnumMap, json['recording_method']) ?? + _$RecordingMethodEnumMap, json['recordingMethod']) ?? RecordingMethod.unknown, - workoutSummary: json['workout_summary'] == null + workoutSummary: json['workoutSummary'] == null ? null : WorkoutSummary.fromJson( - json['workout_summary'] as Map), + json['workoutSummary'] as Map), metadata: json['metadata'] as Map?, ); Map _$HealthDataPointToJson(HealthDataPoint instance) { final val = { 'uuid': instance.uuid, - 'value': instance.value, + 'value': instance.value.toJson(), 'type': _$HealthDataTypeEnumMap[instance.type]!, 'unit': _$HealthDataUnitEnumMap[instance.unit]!, - 'date_from': instance.dateFrom.toIso8601String(), - 'date_to': instance.dateTo.toIso8601String(), - 'source_platform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, - 'source_device_id': instance.sourceDeviceId, - 'source_id': instance.sourceId, - 'source_name': instance.sourceName, - 'recording_method': _$RecordingMethodEnumMap[instance.recordingMethod]!, + 'dateFrom': instance.dateFrom.toIso8601String(), + 'dateTo': instance.dateTo.toIso8601String(), + 'sourcePlatform': _$HealthPlatformTypeEnumMap[instance.sourcePlatform]!, + 'sourceDeviceId': instance.sourceDeviceId, + 'sourceId': instance.sourceId, + 'sourceName': instance.sourceName, + 'recordingMethod': _$RecordingMethodEnumMap[instance.recordingMethod]!, }; void writeNotNull(String key, dynamic value) { @@ -50,7 +50,7 @@ Map _$HealthDataPointToJson(HealthDataPoint instance) { } } - writeNotNull('workout_summary', instance.workoutSummary); + writeNotNull('workoutSummary', instance.workoutSummary?.toJson()); writeNotNull('metadata', instance.metadata); return val; } @@ -238,7 +238,7 @@ Map _$HealthValueToJson(HealthValue instance) { NumericHealthValue _$NumericHealthValueFromJson(Map json) => NumericHealthValue( - numericValue: json['numeric_value'] as num, + numericValue: json['numericValue'] as num, )..$type = json['__type'] as String?; Map _$NumericHealthValueToJson(NumericHealthValue instance) { @@ -251,7 +251,7 @@ Map _$NumericHealthValueToJson(NumericHealthValue instance) { } writeNotNull('__type', instance.$type); - val['numeric_value'] = instance.numericValue; + val['numericValue'] = instance.numericValue; return val; } @@ -260,10 +260,10 @@ AudiogramHealthValue _$AudiogramHealthValueFromJson( AudiogramHealthValue( frequencies: (json['frequencies'] as List).map((e) => e as num).toList(), - leftEarSensitivities: (json['left_ear_sensitivities'] as List) + leftEarSensitivities: (json['leftEarSensitivities'] as List) .map((e) => e as num) .toList(), - rightEarSensitivities: (json['right_ear_sensitivities'] as List) + rightEarSensitivities: (json['rightEarSensitivities'] as List) .map((e) => e as num) .toList(), )..$type = json['__type'] as String?; @@ -280,24 +280,24 @@ Map _$AudiogramHealthValueToJson( writeNotNull('__type', instance.$type); val['frequencies'] = instance.frequencies; - val['left_ear_sensitivities'] = instance.leftEarSensitivities; - val['right_ear_sensitivities'] = instance.rightEarSensitivities; + val['leftEarSensitivities'] = instance.leftEarSensitivities; + val['rightEarSensitivities'] = instance.rightEarSensitivities; return val; } WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) => WorkoutHealthValue( workoutActivityType: $enumDecode( - _$HealthWorkoutActivityTypeEnumMap, json['workout_activity_type']), - totalEnergyBurned: (json['total_energy_burned'] as num?)?.toInt(), + _$HealthWorkoutActivityTypeEnumMap, json['workoutActivityType']), + totalEnergyBurned: (json['totalEnergyBurned'] as num?)?.toInt(), totalEnergyBurnedUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['total_energy_burned_unit']), - totalDistance: (json['total_distance'] as num?)?.toInt(), + _$HealthDataUnitEnumMap, json['totalEnergyBurnedUnit']), + totalDistance: (json['totalDistance'] as num?)?.toInt(), totalDistanceUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['total_distance_unit']), - totalSteps: (json['total_steps'] as num?)?.toInt(), - totalStepsUnit: $enumDecodeNullable( - _$HealthDataUnitEnumMap, json['total_steps_unit']), + _$HealthDataUnitEnumMap, json['totalDistanceUnit']), + totalSteps: (json['totalSteps'] as num?)?.toInt(), + totalStepsUnit: + $enumDecodeNullable(_$HealthDataUnitEnumMap, json['totalStepsUnit']), )..$type = json['__type'] as String?; Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) { @@ -310,17 +310,17 @@ Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) { } writeNotNull('__type', instance.$type); - val['workout_activity_type'] = + val['workoutActivityType'] = _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!; - writeNotNull('total_energy_burned', instance.totalEnergyBurned); - writeNotNull('total_energy_burned_unit', + writeNotNull('totalEnergyBurned', instance.totalEnergyBurned); + writeNotNull('totalEnergyBurnedUnit', _$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit]); - writeNotNull('total_distance', instance.totalDistance); - writeNotNull('total_distance_unit', - _$HealthDataUnitEnumMap[instance.totalDistanceUnit]); - writeNotNull('total_steps', instance.totalSteps); + writeNotNull('totalDistance', instance.totalDistance); writeNotNull( - 'total_steps_unit', _$HealthDataUnitEnumMap[instance.totalStepsUnit]); + 'totalDistanceUnit', _$HealthDataUnitEnumMap[instance.totalDistanceUnit]); + writeNotNull('totalSteps', instance.totalSteps); + writeNotNull( + 'totalStepsUnit', _$HealthDataUnitEnumMap[instance.totalStepsUnit]); return val; } @@ -432,12 +432,12 @@ const _$HealthWorkoutActivityTypeEnumMap = { ElectrocardiogramHealthValue _$ElectrocardiogramHealthValueFromJson( Map json) => ElectrocardiogramHealthValue( - voltageValues: (json['voltage_values'] as List) + voltageValues: (json['voltageValues'] as List) .map((e) => ElectrocardiogramVoltageValue.fromJson(e as Map)) .toList(), - averageHeartRate: json['average_heart_rate'] as num?, - samplingFrequency: (json['sampling_frequency'] as num?)?.toDouble(), + averageHeartRate: json['averageHeartRate'] as num?, + samplingFrequency: (json['samplingFrequency'] as num?)?.toDouble(), classification: $enumDecodeNullable( _$ElectrocardiogramClassificationEnumMap, json['classification']), )..$type = json['__type'] as String?; @@ -453,9 +453,9 @@ Map _$ElectrocardiogramHealthValueToJson( } writeNotNull('__type', instance.$type); - val['voltage_values'] = instance.voltageValues; - writeNotNull('average_heart_rate', instance.averageHeartRate); - writeNotNull('sampling_frequency', instance.samplingFrequency); + val['voltageValues'] = instance.voltageValues.map((e) => e.toJson()).toList(); + writeNotNull('averageHeartRate', instance.averageHeartRate); + writeNotNull('samplingFrequency', instance.samplingFrequency); writeNotNull('classification', _$ElectrocardiogramClassificationEnumMap[instance.classification]); return val; @@ -479,7 +479,7 @@ ElectrocardiogramVoltageValue _$ElectrocardiogramVoltageValueFromJson( Map json) => ElectrocardiogramVoltageValue( voltage: json['voltage'] as num, - timeSinceSampleStart: json['time_since_sample_start'] as num, + timeSinceSampleStart: json['timeSinceSampleStart'] as num, )..$type = json['__type'] as String?; Map _$ElectrocardiogramVoltageValueToJson( @@ -494,7 +494,7 @@ Map _$ElectrocardiogramVoltageValueToJson( writeNotNull('__type', instance.$type); val['voltage'] = instance.voltage; - val['time_since_sample_start'] = instance.timeSinceSampleStart; + val['timeSinceSampleStart'] = instance.timeSinceSampleStart; return val; } @@ -531,36 +531,36 @@ NutritionHealthValue _$NutritionHealthValueFromJson( Map json) => NutritionHealthValue( name: json['name'] as String?, - mealType: json['meal_type'] as String?, + mealType: json['mealType'] as String?, calories: (json['calories'] as num?)?.toDouble(), protein: (json['protein'] as num?)?.toDouble(), fat: (json['fat'] as num?)?.toDouble(), carbs: (json['carbs'] as num?)?.toDouble(), caffeine: (json['caffeine'] as num?)?.toDouble(), - vitaminA: (json['vitamin_a'] as num?)?.toDouble(), - b1Thiamine: (json['b1_thiamine'] as num?)?.toDouble(), - b2Riboflavin: (json['b2_riboflavin'] as num?)?.toDouble(), - b3Niacin: (json['b3_niacin'] as num?)?.toDouble(), - b5PantothenicAcid: (json['b5_pantothenic_acid'] as num?)?.toDouble(), - b6Pyridoxine: (json['b6_pyridoxine'] as num?)?.toDouble(), - b7Biotin: (json['b7_biotin'] as num?)?.toDouble(), - b9Folate: (json['b9_folate'] as num?)?.toDouble(), - b12Cobalamin: (json['b12_cobalamin'] as num?)?.toDouble(), - vitaminC: (json['vitamin_c'] as num?)?.toDouble(), - vitaminD: (json['vitamin_d'] as num?)?.toDouble(), - vitaminE: (json['vitamin_e'] as num?)?.toDouble(), - vitaminK: (json['vitamin_k'] as num?)?.toDouble(), + vitaminA: (json['vitaminA'] as num?)?.toDouble(), + b1Thiamine: (json['b1Thiamine'] as num?)?.toDouble(), + b2Riboflavin: (json['b2Riboflavin'] as num?)?.toDouble(), + b3Niacin: (json['b3Niacin'] as num?)?.toDouble(), + b5PantothenicAcid: (json['b5PantothenicAcid'] as num?)?.toDouble(), + b6Pyridoxine: (json['b6Pyridoxine'] as num?)?.toDouble(), + b7Biotin: (json['b7Biotin'] as num?)?.toDouble(), + b9Folate: (json['b9Folate'] as num?)?.toDouble(), + b12Cobalamin: (json['b12Cobalamin'] as num?)?.toDouble(), + vitaminC: (json['vitaminC'] as num?)?.toDouble(), + vitaminD: (json['vitaminD'] as num?)?.toDouble(), + vitaminE: (json['vitaminE'] as num?)?.toDouble(), + vitaminK: (json['vitaminK'] as num?)?.toDouble(), calcium: (json['calcium'] as num?)?.toDouble(), chloride: (json['chloride'] as num?)?.toDouble(), cholesterol: (json['cholesterol'] as num?)?.toDouble(), choline: (json['choline'] as num?)?.toDouble(), chromium: (json['chromium'] as num?)?.toDouble(), copper: (json['copper'] as num?)?.toDouble(), - fatUnsaturated: (json['fat_unsaturated'] as num?)?.toDouble(), - fatMonounsaturated: (json['fat_monounsaturated'] as num?)?.toDouble(), - fatPolyunsaturated: (json['fat_polyunsaturated'] as num?)?.toDouble(), - fatSaturated: (json['fat_saturated'] as num?)?.toDouble(), - fatTransMonoenoic: (json['fat_trans_monoenoic'] as num?)?.toDouble(), + fatUnsaturated: (json['fatUnsaturated'] as num?)?.toDouble(), + fatMonounsaturated: (json['fatMonounsaturated'] as num?)?.toDouble(), + fatPolyunsaturated: (json['fatPolyunsaturated'] as num?)?.toDouble(), + fatSaturated: (json['fatSaturated'] as num?)?.toDouble(), + fatTransMonoenoic: (json['fatTransMonoenoic'] as num?)?.toDouble(), fiber: (json['fiber'] as num?)?.toDouble(), iodine: (json['iodine'] as num?)?.toDouble(), iron: (json['iron'] as num?)?.toDouble(), @@ -588,36 +588,36 @@ Map _$NutritionHealthValueToJson( writeNotNull('__type', instance.$type); writeNotNull('name', instance.name); - writeNotNull('meal_type', instance.mealType); + writeNotNull('mealType', instance.mealType); writeNotNull('calories', instance.calories); writeNotNull('protein', instance.protein); writeNotNull('fat', instance.fat); writeNotNull('carbs', instance.carbs); writeNotNull('caffeine', instance.caffeine); - writeNotNull('vitamin_a', instance.vitaminA); - writeNotNull('b1_thiamine', instance.b1Thiamine); - writeNotNull('b2_riboflavin', instance.b2Riboflavin); - writeNotNull('b3_niacin', instance.b3Niacin); - writeNotNull('b5_pantothenic_acid', instance.b5PantothenicAcid); - writeNotNull('b6_pyridoxine', instance.b6Pyridoxine); - writeNotNull('b7_biotin', instance.b7Biotin); - writeNotNull('b9_folate', instance.b9Folate); - writeNotNull('b12_cobalamin', instance.b12Cobalamin); - writeNotNull('vitamin_c', instance.vitaminC); - writeNotNull('vitamin_d', instance.vitaminD); - writeNotNull('vitamin_e', instance.vitaminE); - writeNotNull('vitamin_k', instance.vitaminK); + writeNotNull('vitaminA', instance.vitaminA); + writeNotNull('b1Thiamine', instance.b1Thiamine); + writeNotNull('b2Riboflavin', instance.b2Riboflavin); + writeNotNull('b3Niacin', instance.b3Niacin); + writeNotNull('b5PantothenicAcid', instance.b5PantothenicAcid); + writeNotNull('b6Pyridoxine', instance.b6Pyridoxine); + writeNotNull('b7Biotin', instance.b7Biotin); + writeNotNull('b9Folate', instance.b9Folate); + writeNotNull('b12Cobalamin', instance.b12Cobalamin); + writeNotNull('vitaminC', instance.vitaminC); + writeNotNull('vitaminD', instance.vitaminD); + writeNotNull('vitaminE', instance.vitaminE); + writeNotNull('vitaminK', instance.vitaminK); writeNotNull('calcium', instance.calcium); writeNotNull('chloride', instance.chloride); writeNotNull('cholesterol', instance.cholesterol); writeNotNull('choline', instance.choline); writeNotNull('chromium', instance.chromium); writeNotNull('copper', instance.copper); - writeNotNull('fat_unsaturated', instance.fatUnsaturated); - writeNotNull('fat_monounsaturated', instance.fatMonounsaturated); - writeNotNull('fat_polyunsaturated', instance.fatPolyunsaturated); - writeNotNull('fat_saturated', instance.fatSaturated); - writeNotNull('fat_trans_monoenoic', instance.fatTransMonoenoic); + writeNotNull('fatUnsaturated', instance.fatUnsaturated); + writeNotNull('fatMonounsaturated', instance.fatMonounsaturated); + writeNotNull('fatPolyunsaturated', instance.fatPolyunsaturated); + writeNotNull('fatSaturated', instance.fatSaturated); + writeNotNull('fatTransMonoenoic', instance.fatTransMonoenoic); writeNotNull('fiber', instance.fiber); writeNotNull('iodine', instance.iodine); writeNotNull('iron', instance.iron); @@ -638,9 +638,9 @@ MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson( Map json) => MenstruationFlowHealthValue( flow: $enumDecodeNullable(_$MenstrualFlowEnumMap, json['flow']), - dateTime: DateTime.parse(json['date_time'] as String), - isStartOfCycle: json['is_start_of_cycle'] as bool?, - wasUserEntered: json['was_user_entered'] as bool?, + dateTime: DateTime.parse(json['dateTime'] as String), + isStartOfCycle: json['isStartOfCycle'] as bool?, + wasUserEntered: json['wasUserEntered'] as bool?, )..$type = json['__type'] as String?; Map _$MenstruationFlowHealthValueToJson( @@ -655,9 +655,9 @@ Map _$MenstruationFlowHealthValueToJson( writeNotNull('__type', instance.$type); writeNotNull('flow', _$MenstrualFlowEnumMap[instance.flow]); - writeNotNull('is_start_of_cycle', instance.isStartOfCycle); - writeNotNull('was_user_entered', instance.wasUserEntered); - val['date_time'] = instance.dateTime.toIso8601String(); + writeNotNull('isStartOfCycle', instance.isStartOfCycle); + writeNotNull('wasUserEntered', instance.wasUserEntered); + val['dateTime'] = instance.dateTime.toIso8601String(); return val; } @@ -672,16 +672,16 @@ const _$MenstrualFlowEnumMap = { WorkoutSummary _$WorkoutSummaryFromJson(Map json) => WorkoutSummary( - workoutType: json['workout_type'] as String, - totalDistance: json['total_distance'] as num, - totalEnergyBurned: json['total_energy_burned'] as num, - totalSteps: json['total_steps'] as num, + workoutType: json['workoutType'] as String, + totalDistance: json['totalDistance'] as num, + totalEnergyBurned: json['totalEnergyBurned'] as num, + totalSteps: json['totalSteps'] as num, ); Map _$WorkoutSummaryToJson(WorkoutSummary instance) => { - 'workout_type': instance.workoutType, - 'total_distance': instance.totalDistance, - 'total_energy_burned': instance.totalEnergyBurned, - 'total_steps': instance.totalSteps, + 'workoutType': instance.workoutType, + 'totalDistance': instance.totalDistance, + 'totalEnergyBurned': instance.totalEnergyBurned, + 'totalSteps': instance.totalSteps, }; diff --git a/packages/health/lib/src/health_data_point.dart b/packages/health/lib/src/health_data_point.dart index 2598a0e1b..029af835c 100644 --- a/packages/health/lib/src/health_data_point.dart +++ b/packages/health/lib/src/health_data_point.dart @@ -6,7 +6,7 @@ enum HealthPlatformType { appleHealth, googleHealthConnect } /// A [HealthDataPoint] object corresponds to a data point capture from /// Apple HealthKit or Google Health Connect with a [HealthValue] /// as value. -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class HealthDataPoint { /// UUID of the data point. String uuid; diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart index d313104ca..4b88adc50 100644 --- a/packages/health/lib/src/health_value_types.dart +++ b/packages/health/lib/src/health_value_types.dart @@ -1,14 +1,14 @@ part of '../health.dart'; /// An abstract class for health values. -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class HealthValue extends Serializable { HealthValue(); @override Function get fromJsonFunction => _$HealthValueFromJson; factory HealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as HealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$HealthValueToJson(this); } @@ -18,7 +18,7 @@ class HealthValue extends Serializable { /// /// Parameters: /// * [numericValue] - a [num] value for the [HealthDataPoint] -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class NumericHealthValue extends HealthValue { /// A [num] value for the [HealthDataPoint]. num numericValue; @@ -35,7 +35,7 @@ class NumericHealthValue extends HealthValue { @override Function get fromJsonFunction => _$NumericHealthValueFromJson; factory NumericHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as NumericHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$NumericHealthValueToJson(this); @@ -53,7 +53,7 @@ class NumericHealthValue extends HealthValue { /// * [frequencies] - array of frequencies of the test /// * [leftEarSensitivities] threshold in decibel for the left ear /// * [rightEarSensitivities] threshold in decibel for the left ear -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class AudiogramHealthValue extends HealthValue { /// Array of frequencies of the test. List frequencies; @@ -87,7 +87,7 @@ class AudiogramHealthValue extends HealthValue { @override Function get fromJsonFunction => _$AudiogramHealthValueFromJson; factory AudiogramHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as AudiogramHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$AudiogramHealthValueToJson(this); @@ -111,7 +111,7 @@ class AudiogramHealthValue extends HealthValue { /// * [totalEnergyBurnedUnit] - the unit of the total energy burned /// * [totalDistance] - the total distance of the workout /// * [totalDistanceUnit] - the unit of the total distance -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class WorkoutHealthValue extends HealthValue { /// The type of the workout. HealthWorkoutActivityType workoutActivityType; @@ -181,7 +181,7 @@ class WorkoutHealthValue extends HealthValue { @override Function get fromJsonFunction => _$WorkoutHealthValueFromJson; factory WorkoutHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as WorkoutHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$WorkoutHealthValueToJson(this); @@ -224,7 +224,7 @@ class WorkoutHealthValue extends HealthValue { /// * [averageHeartRate] - the average heart rate during the ECG (in BPM) /// * [samplingFrequency] - the frequency at which the Apple Watch sampled the voltage. /// * [classification] - an [ElectrocardiogramClassification] -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class ElectrocardiogramHealthValue extends HealthValue { /// An array of [ElectrocardiogramVoltageValue]s. List voltageValues; @@ -248,7 +248,7 @@ class ElectrocardiogramHealthValue extends HealthValue { @override Function get fromJsonFunction => _$ElectrocardiogramHealthValueFromJson; factory ElectrocardiogramHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as ElectrocardiogramHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$ElectrocardiogramHealthValueToJson(this); @@ -283,7 +283,7 @@ class ElectrocardiogramHealthValue extends HealthValue { } /// Single voltage value belonging to a [ElectrocardiogramHealthValue] -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class ElectrocardiogramVoltageValue extends HealthValue { /// Voltage of the ECG. num voltage; @@ -306,7 +306,7 @@ class ElectrocardiogramVoltageValue extends HealthValue { @override Function get fromJsonFunction => _$ElectrocardiogramVoltageValueFromJson; factory ElectrocardiogramVoltageValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as ElectrocardiogramVoltageValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$ElectrocardiogramVoltageValueToJson(this); @@ -324,7 +324,7 @@ class ElectrocardiogramVoltageValue extends HealthValue { } /// A [HealthValue] object from insulin delivery (iOS only) -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class InsulinDeliveryHealthValue extends HealthValue { /// The amount of units of insulin taken double units; @@ -355,7 +355,7 @@ class InsulinDeliveryHealthValue extends HealthValue { @override Function get fromJsonFunction => _$InsulinDeliveryHealthValueFromJson; factory InsulinDeliveryHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as InsulinDeliveryHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$InsulinDeliveryHealthValueToJson(this); @@ -420,7 +420,7 @@ class InsulinDeliveryHealthValue extends HealthValue { /// * [water] - the amount of water in grams /// * [zinc] - the amount of zinc in grams -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class NutritionHealthValue extends HealthValue { /// The name of the food. String? name; @@ -604,7 +604,7 @@ class NutritionHealthValue extends HealthValue { @override Function get fromJsonFunction => _$NutritionHealthValueFromJson; factory NutritionHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as NutritionHealthValue; + (json) as NutritionHealthValue; @override Map toJson() => _$NutritionHealthValueToJson(this); @@ -865,7 +865,7 @@ enum RecordingMethod { /// * [isStartOfCycle] - indicator whether or not this occurrence is the first day of the menstrual cycle (iOS only) /// * [wasUserEntered] - indicator whether or not the data was entered by the user (iOS only) /// * [dateTime] - the date and time of the menstrual flow -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class MenstruationFlowHealthValue extends HealthValue { final MenstrualFlow? flow; final bool? isStartOfCycle; @@ -912,7 +912,7 @@ class MenstruationFlowHealthValue extends HealthValue { Function get fromJsonFunction => _$MenstruationFlowHealthValueFromJson; factory MenstruationFlowHealthValue.fromJson(Map json) => - FromJsonFactory().fromJson(json) as MenstruationFlowHealthValue; + FromJsonFactory().fromJson(json); @override Map toJson() => _$MenstruationFlowHealthValueToJson(this); diff --git a/packages/health/lib/src/workout_summary.dart b/packages/health/lib/src/workout_summary.dart index c36613802..14682a402 100644 --- a/packages/health/lib/src/workout_summary.dart +++ b/packages/health/lib/src/workout_summary.dart @@ -6,7 +6,7 @@ part of '../health.dart'; /// * [totalDistance] - The total distance that was traveled during a workout. /// * [totalEnergyBurned] - The amount of energy that was burned during a workout. /// * [totalSteps] - The number of steps during a workout. -@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false) +@JsonSerializable(includeIfNull: false, explicitToJson: true) class WorkoutSummary { /// Workout type. String workoutType; diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index ffc1be0af..56339aba8 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: intl: '>=0.18.0 <0.20.0' device_info_plus: '>=9.0.0 <11.0.0' json_annotation: ^4.8.0 - carp_serializable: ^1.1.0 # polymorphic json serialization + carp_serializable: ^2.0.0 # polymorphic json serialization dev_dependencies: flutter_test: From 27ac35d032d0cf013db1d18c6835b9791084a4bb Mon Sep 17 00:00:00 2001 From: bardram Date: Mon, 23 Sep 2024 16:46:54 +0200 Subject: [PATCH 5/5] Update pubspec.yaml --- packages/health/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index 56339aba8..7690d1c50 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -1,6 +1,6 @@ name: health description: Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android. -version: 11.0.0 +version: 11.1.0 homepage: https://github.com/cph-cachet/flutter-plugins/tree/master/packages/health environment: