diff --git a/.gitignore b/.gitignore
index f66dbae2d..8bb6a7a81 100644
--- a/.gitignore
+++ b/.gitignore
@@ -119,3 +119,5 @@ app.*.symbols
!/dev/ci/**/Gemfile.lock
packages/app_usage/example/.flutter-plugins-dependencies
packages/app_usage/example/.flutter-plugins-dependencies
+
+.sdkmanrc
\ No newline at end of file
diff --git a/packages/health/CHANGELOG.md b/packages/health/CHANGELOG.md
index d1e668fd7..7753e1d0e 100644
--- a/packages/health/CHANGELOG.md
+++ b/packages/health/CHANGELOG.md
@@ -1,3 +1,34 @@
+## 12.0.0
+
+* **BREAKING** This release introduces a significant architectural change to the `health` plugin by removing the `singleton` pattern.
+ * **Dependency Injection for `DeviceInfoPlugin`**:
+ - The `Health` class is no longer a singleton.
+ - The `Health()` factory constructor is removed.
+ - The `Health` class now accepts an (optional) `DeviceInfoPlugin` dependency through its constructor, this change was introduced to provide easy mocking of the `DeviceInfo` class during unit tests.
+ - This architectural change means that, for the application to work correctly, the `Health` class *MUST* be initialized correctly as a global instance.
+ * **Impact**:
+ - For most users, **no immediate code changes are required** but it is paramount to initialize the `Health` class as a global instance (i.e. do not call `Health()` every time but rather define an instance `final health = Health();`).
+* **BREAKING** (Android) Remove automatic permission request of `DISTANCE_DELTA` and `TOTAL_CALORIES_BURNED` data types when requesting permission for `WORKOUT` health data type.
+ * For `WORKOUT`s that require above permissions, now those need to be requested manually.
+ * Fix [#984](https://github.com/cph-cachet/flutter-plugins/issues/984) - PR [#1055](https://github.com/cph-cachet/flutter-plugins/pull/1055)
+* Add `LEAN_BODY_MASS` data type [#1078](https://github.com/cph-cachet/flutter-plugins/issues/1078) - PR [#1097](https://github.com/cph-cachet/flutter-plugins/pull/1097)
+ * The following AndroidManifest values are required to READ/WRITE `LEAN_BODY_MASS`:
+ ```XML
+
+
+ ```
+* iOS: Add `WATER_TEMPERATURE` and `UNDERWATER_DEPTH` health values [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096)
+* iOS: Add support for `Underwater Diving` workout [#1096](https://github.com/cph-cachet/flutter-plugins/issues/1096)
+* Fix [#1072](https://github.com/cph-cachet/flutter-plugins/issues/1072) and [#1074](https://github.com/cph-cachet/flutter-plugins/issues/1074)
+* Fix issue where iOS delete not deleting own records - PR [#1104](https://github.com/cph-cachet/flutter-plugins/pull/1104)
+* Fix [#950](https://github.com/cph-cachet/flutter-plugins/issues/950) - PR [#1103](https://github.com/cph-cachet/flutter-plugins/pull/1103)
+* Fix [#1047](https://github.com/cph-cachet/flutter-plugins/issues/1047) and [#939](https://github.com/cph-cachet/flutter-plugins/issues/939) - PR [#1091](https://github.com/cph-cachet/flutter-plugins/pull/1091)
+* Fix issue where `SLEEP_LIGHT` type was not aligned correctly - PR [#1086](https://github.com/cph-cachet/flutter-plugins/pull/1086)
+* Fix [#1051](https://github.com/cph-cachet/flutter-plugins/issues/1051) - PR [#1052](https://github.com/cph-cachet/flutter-plugins/pull/1052)
+* Updated `intl` to ^0.20.1 [#1092](https://github.com/cph-cachet/flutter-plugins/issues/1092)
+* Updated `device_info_plus` to ^11.2.0
+* Example app: Updated `permission_handler` to ^11.3.1
+
## 11.1.1
* Fix of [#1059](https://github.com/cph-cachet/flutter-plugins/issues/1059)
diff --git a/packages/health/README.md b/packages/health/README.md
index 68ec291bc..eea60737c 100644
--- a/packages/health/README.md
+++ b/packages/health/README.md
@@ -355,6 +355,10 @@ The plugin supports the following [`HealthDataType`](https://pub.dev/documentati
| ELECTROCARDIOGRAM | VOLT | yes | | Requires Apple Watch to write the data |
| NUTRITION | NO_UNIT | yes | yes | |
| INSULIN_DELIVERY | INTERNATIONAL_UNIT | yes | | |
+| MENSTRUATION_FLOW | NO_UNIT | yes | yes | |
+| WATER_TEMPERATURE | DEGREE_CELSIUS | yes | | Related to/Requires Apple Watch Ultra's Underwater Diving Workout |
+| UNDERWATER_DEPTH | METER | yes | | Related to/Requires Apple Watch Ultra's Underwater Diving Workout |
+| LEAN_BODY_MASS | KILOGRAMS | yes | yes | |
## Workout Types
@@ -443,6 +447,7 @@ The plugin supports the following [`HealthWorkoutActivityType`](https://pub.dev/
| TENNIS | yes | yes | |
| TRACK_AND_FIELD | yes | | |
| TRADITIONAL_STRENGTH_TRAINING | yes | (yes) | on Android this will be stored as STRENGTH_TRAINING |
+| UNDERWATER_DIVING | yes | | |
| VOLLEYBALL | yes | yes | |
| WALKING | yes | yes | |
| WATER_FITNESS | yes | | |
diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt
index 216aff7ea..1d6ee83dc 100644
--- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt
+++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt
@@ -48,6 +48,7 @@ const val BLOOD_OXYGEN = "BLOOD_OXYGEN"
const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC"
const val BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC"
const val BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE"
+const val LEAN_BODY_MASS = "LEAN_BODY_MASS"
const val BODY_TEMPERATURE = "BODY_TEMPERATURE"
const val BODY_WATER_MASS = "BODY_WATER_MASS"
const val DISTANCE_DELTA = "DISTANCE_DELTA"
@@ -544,38 +545,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
),
)
}
- // Workout also needs distance and total energy burned too
- if (typeKey == WORKOUT) {
- if (access == 0) {
- permList.addAll(
- listOf(
- HealthPermission.getReadPermission(
- DistanceRecord::class
- ),
- HealthPermission.getReadPermission(
- TotalCaloriesBurnedRecord::class
- ),
- ),
- )
- } else {
- permList.addAll(
- listOf(
- HealthPermission.getReadPermission(
- DistanceRecord::class
- ),
- HealthPermission.getReadPermission(
- TotalCaloriesBurnedRecord::class
- ),
- HealthPermission.getWritePermission(
- DistanceRecord::class
- ),
- HealthPermission.getWritePermission(
- TotalCaloriesBurnedRecord::class
- ),
- ),
- )
- }
- }
}
scope.launch {
result.success(
@@ -629,38 +598,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
),
)
}
- // Workout also needs distance and total energy burned too
- if (typeKey == WORKOUT) {
- if (access == 0) {
- permList.addAll(
- listOf(
- HealthPermission.getReadPermission(
- DistanceRecord::class
- ),
- HealthPermission.getReadPermission(
- TotalCaloriesBurnedRecord::class
- ),
- ),
- )
- } else {
- permList.addAll(
- listOf(
- HealthPermission.getReadPermission(
- DistanceRecord::class
- ),
- HealthPermission.getReadPermission(
- TotalCaloriesBurnedRecord::class
- ),
- HealthPermission.getWritePermission(
- DistanceRecord::class
- ),
- HealthPermission.getWritePermission(
- TotalCaloriesBurnedRecord::class
- ),
- ),
- )
- }
- }
}
if (healthConnectRequestPermissionsLauncher == null) {
result.success(false)
@@ -1083,6 +1020,29 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
),
)
+ is LeanBodyMassRecord ->
+ return listOf(
+ mapOf(
+ "uuid" to
+ metadata.id,
+ "value" to
+ record.mass
+ .inKilograms,
+ "date_from" to
+ record.time
+ .toEpochMilli(),
+ "date_to" to
+ record.time
+ .toEpochMilli(),
+ "source_id" to "",
+ "source_name" to
+ metadata.dataOrigin
+ .packageName,
+ "recording_method" to
+ metadata.recordingMethod
+ ),
+ )
+
is StepsRecord ->
return listOf(
mapOf(
@@ -1516,7 +1476,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
"sugar" to record.sugar?.inGrams,
"water" to null,
"zinc" to record.zinc?.inGrams,
- "name" to record.name!!,
+ "name" to (record.name ?: ""),
"meal_type" to
(mapTypeToMealType[
record.mealType]
@@ -1596,6 +1556,22 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
),
)
+ LEAN_BODY_MASS ->
+ LeanBodyMassRecord(
+ time =
+ Instant.ofEpochMilli(
+ startTime
+ ),
+ mass =
+ Mass.kilograms(
+ value
+ ),
+ zoneOffset = null,
+ metadata = Metadata(
+ recordingMethod = recordingMethod,
+ ),
+ )
+
HEIGHT ->
HeightRecord(
time =
@@ -2398,6 +2374,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
private val mapToType =
hashMapOf(
BODY_FAT_PERCENTAGE to BodyFatRecord::class,
+ LEAN_BODY_MASS to LeanBodyMassRecord::class,
HEIGHT to HeightRecord::class,
WEIGHT to WeightRecord::class,
STEPS to StepsRecord::class,
@@ -2438,7 +2415,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
// "BasalMetabolicRate" to BasalMetabolicRateRecord::class,
// "BloodGlucose" to BloodGlucoseRecord::class,
// "BloodPressure" to BloodPressureRecord::class,
- // "BodyFat" to BodyFatRecord::class,
// "BodyTemperature" to BodyTemperatureRecord::class,
// "BoneMass" to BoneMassRecord::class,
// "CervicalMucus" to CervicalMucusRecord::class,
@@ -2451,7 +2427,6 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
// "HeartRate" to HeartRateRecord::class,
// "Height" to HeightRecord::class,
// "Hydration" to HydrationRecord::class,
- // "LeanBodyMass" to LeanBodyMassRecord::class,
// "MenstruationPeriod" to MenstruationPeriodRecord::class,
// "Nutrition" to NutritionRecord::class,
// "OvulationTest" to OvulationTestRecord::class,
diff --git a/packages/health/example/android/app/src/main/AndroidManifest.xml b/packages/health/example/android/app/src/main/AndroidManifest.xml
index f6d185ace..996adaf97 100644
--- a/packages/health/example/android/app/src/main/AndroidManifest.xml
+++ b/packages/health/example/android/app/src/main/AndroidManifest.xml
@@ -65,6 +65,8 @@
+
+
diff --git a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj
index 4ce55d34f..77f2ffb80 100644
--- a/packages/health/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/health/example/ios/Runner.xcodeproj/project.pbxproj
@@ -155,6 +155,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
8AB8966E9F27B6C816D51EA9 /* [CP] Embed Pods Frameworks */,
+ 99FD4B47838A33EC942FDC35 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -271,6 +272,24 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
+ 99FD4B47838A33EC942FDC35 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
+ "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
AFF7CCF5217A091E1625CD54 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -293,6 +312,24 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
+ FEE6262278064D703A8D8D21 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh",
+ "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -394,7 +431,10 @@
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
@@ -531,7 +571,10 @@
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
@@ -564,7 +607,10 @@
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
- LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
diff --git a/packages/health/example/ios/Runner/AppDelegate.swift b/packages/health/example/ios/Runner/AppDelegate.swift
index 70693e4a8..b63630348 100644
--- a/packages/health/example/ios/Runner/AppDelegate.swift
+++ b/packages/health/example/ios/Runner/AppDelegate.swift
@@ -1,7 +1,7 @@
import UIKit
import Flutter
-@UIApplicationMain
+@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart
index db1069ddf..59c963dac 100644
--- a/packages/health/example/lib/main.dart
+++ b/packages/health/example/lib/main.dart
@@ -7,11 +7,16 @@ import 'package:health_example/util.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:carp_serializable/carp_serializable.dart';
+// Global Health instance
+final health = Health();
+
void main() => runApp(HealthApp());
class HealthApp extends StatefulWidget {
+ const HealthApp({super.key});
+
@override
- _HealthAppState createState() => _HealthAppState();
+ HealthAppState createState() => HealthAppState();
}
enum AppState {
@@ -32,7 +37,7 @@ enum AppState {
PERMISSIONS_NOT_REVOKED,
}
-class _HealthAppState extends State {
+class HealthAppState extends State {
List _healthDataList = [];
AppState _state = AppState.DATA_NOT_FETCHED;
int _nofSteps = 0;
@@ -83,15 +88,15 @@ class _HealthAppState extends State {
@override
void initState() {
// configure the health plugin before use and check the Health Connect status
- Health().configure();
- Health().getHealthConnectSdkStatus();
+ health.configure();
+ health.getHealthConnectSdkStatus();
super.initState();
}
/// Install Google Health Connect on this phone.
Future installHealthConnect() async =>
- await Health().installHealthConnect();
+ await health.installHealthConnect();
/// Authorize, i.e. get permissions to access relevant health data.
Future authorize() async {
@@ -105,7 +110,7 @@ class _HealthAppState extends State {
// Check if we have health permissions
bool? hasPermissions =
- await Health().hasPermissions(types, permissions: permissions);
+ await health.hasPermissions(types, permissions: permissions);
// hasPermissions = false because the hasPermission cannot disclose if WRITE access exists.
// Hence, we have to request with WRITE as well.
@@ -115,8 +120,8 @@ class _HealthAppState extends State {
if (!hasPermissions) {
// requesting access to the data types before reading them
try {
- authorized = await Health()
- .requestAuthorization(types, permissions: permissions);
+ authorized =
+ await health.requestAuthorization(types, permissions: permissions);
} catch (error) {
debugPrint("Exception in authorize: $error");
}
@@ -130,7 +135,7 @@ class _HealthAppState extends State {
Future getHealthConnectSdkStatus() async {
assert(Platform.isAndroid, "This is only available on Android");
- final status = await Health().getHealthConnectSdkStatus();
+ final status = await health.getHealthConnectSdkStatus();
setState(() {
_contentHealthConnectStatus =
@@ -152,7 +157,7 @@ class _HealthAppState extends State {
try {
// fetch health data
- List healthData = await Health().getHealthDataFromTypes(
+ List healthData = await health.getHealthDataFromTypes(
types: types,
startTime: yesterday,
endTime: now,
@@ -173,9 +178,11 @@ class _HealthAppState extends State {
}
// filter out duplicates
- _healthDataList = Health().removeDuplicates(_healthDataList);
+ _healthDataList = health.removeDuplicates(_healthDataList);
- _healthDataList.forEach((data) => debugPrint(toJsonString(data)));
+ for (var data in _healthDataList) {
+ debugPrint(toJsonString(data));
+ }
// update the UI to display the results
setState(() {
@@ -197,98 +204,104 @@ class _HealthAppState extends State {
bool success = true;
// misc. health data examples using the writeHealthData() method
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 1.925,
type: HealthDataType.HEIGHT,
startTime: earlier,
endTime: now,
recordingMethod: RecordingMethod.manual);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 90,
type: HealthDataType.WEIGHT,
startTime: now,
recordingMethod: RecordingMethod.manual);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 90,
type: HealthDataType.HEART_RATE,
startTime: earlier,
endTime: now,
recordingMethod: RecordingMethod.manual);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 90,
type: HealthDataType.STEPS,
startTime: earlier,
endTime: now,
recordingMethod: RecordingMethod.manual);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 200,
type: HealthDataType.ACTIVE_ENERGY_BURNED,
startTime: earlier,
endTime: now,
);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 70,
type: HealthDataType.HEART_RATE,
startTime: earlier,
endTime: now);
if (Platform.isIOS) {
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 30,
type: HealthDataType.HEART_RATE_VARIABILITY_SDNN,
startTime: earlier,
endTime: now);
} else {
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 30,
type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD,
startTime: earlier,
endTime: now);
}
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 37,
type: HealthDataType.BODY_TEMPERATURE,
startTime: earlier,
endTime: now);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 105,
type: HealthDataType.BLOOD_GLUCOSE,
startTime: earlier,
endTime: now);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 1.8,
type: HealthDataType.WATER,
startTime: earlier,
endTime: now);
// different types of sleep
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 0.0,
type: HealthDataType.SLEEP_REM,
startTime: earlier,
endTime: now);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 0.0,
type: HealthDataType.SLEEP_ASLEEP,
startTime: earlier,
endTime: now);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 0.0,
type: HealthDataType.SLEEP_AWAKE,
startTime: earlier,
endTime: now);
- success &= await Health().writeHealthData(
+ success &= await health.writeHealthData(
value: 0.0,
type: HealthDataType.SLEEP_DEEP,
startTime: earlier,
endTime: now);
+ success &= await health.writeHealthData(
+ value: 22,
+ type: HealthDataType.LEAN_BODY_MASS,
+ startTime: earlier,
+ endTime: now,
+ );
// specialized write methods
- success &= await Health().writeBloodOxygen(
+ success &= await health.writeBloodOxygen(
saturation: 98,
startTime: earlier,
endTime: now,
);
- success &= await Health().writeWorkoutData(
+ success &= await health.writeWorkoutData(
activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL,
title: "Random workout name that shows up in Health Connect",
start: now.subtract(const Duration(minutes: 15)),
@@ -296,12 +309,12 @@ class _HealthAppState extends State {
totalDistance: 2430,
totalEnergyBurned: 400,
);
- success &= await Health().writeBloodPressure(
+ success &= await health.writeBloodPressure(
systolic: 90,
diastolic: 80,
startTime: now,
);
- success &= await Health().writeMeal(
+ success &= await health.writeMeal(
mealType: MealType.SNACK,
startTime: earlier,
endTime: now,
@@ -353,7 +366,7 @@ class _HealthAppState extends State {
// const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0];
// const leftEarSensitivities = [49.0, 54.0, 89.0, 52.0, 77.0, 35.0];
// const rightEarSensitivities = [76.0, 66.0, 90.0, 22.0, 85.0, 44.5];
- // success &= await Health().writeAudiogram(
+ // success &= await health.writeAudiogram(
// frequencies,
// leftEarSensitivities,
// rightEarSensitivities,
@@ -365,13 +378,30 @@ class _HealthAppState extends State {
// },
// );
- success &= await Health().writeMenstruationFlow(
+ success &= await health.writeMenstruationFlow(
flow: MenstrualFlow.medium,
isStartOfCycle: true,
startTime: earlier,
endTime: now,
);
+ // Available on iOS 16.0+ only
+ if (Platform.isIOS) {
+ success &= await health.writeHealthData(
+ value: 22,
+ type: HealthDataType.WATER_TEMPERATURE,
+ startTime: earlier,
+ endTime: now,
+ recordingMethod: RecordingMethod.manual);
+
+ success &= await health.writeHealthData(
+ value: 55,
+ type: HealthDataType.UNDERWATER_DEPTH,
+ startTime: earlier,
+ endTime: now,
+ recordingMethod: RecordingMethod.manual);
+ }
+
setState(() {
_state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED;
});
@@ -384,7 +414,7 @@ class _HealthAppState extends State {
bool success = true;
for (HealthDataType type in types) {
- success &= await Health().delete(
+ success &= await health.delete(
type: type,
startTime: earlier,
endTime: now,
@@ -405,15 +435,15 @@ class _HealthAppState extends State {
final midnight = DateTime(now.year, now.month, now.day);
bool stepsPermission =
- await Health().hasPermissions([HealthDataType.STEPS]) ?? false;
+ await health.hasPermissions([HealthDataType.STEPS]) ?? false;
if (!stepsPermission) {
stepsPermission =
- await Health().requestAuthorization([HealthDataType.STEPS]);
+ await health.requestAuthorization([HealthDataType.STEPS]);
}
if (stepsPermission) {
try {
- steps = await Health().getTotalStepsInInterval(midnight, now,
+ steps = await health.getTotalStepsInInterval(midnight, now,
includeManualEntry:
!recordingMethodsToFilter.contains(RecordingMethod.manual));
} catch (error) {
@@ -439,7 +469,7 @@ class _HealthAppState extends State {
bool success = false;
try {
- await Health().revokePermissions();
+ await health.revokePermissions();
success = true;
} catch (error) {
debugPrint("Exception in revokeAccess: $error");
@@ -474,7 +504,7 @@ class _HealthAppState extends State {
child: const Text("Check Health Connect Status",
style: TextStyle(color: Colors.white))),
if (Platform.isAndroid &&
- Health().healthConnectSdkStatus !=
+ health.healthConnectSdkStatus !=
HealthConnectSdkStatus.sdkAvailable)
TextButton(
onPressed: installHealthConnect,
@@ -484,7 +514,7 @@ class _HealthAppState extends State {
style: TextStyle(color: Colors.white))),
if (Platform.isIOS ||
Platform.isAndroid &&
- Health().healthConnectSdkStatus ==
+ health.healthConnectSdkStatus ==
HealthConnectSdkStatus.sdkAvailable)
Wrap(spacing: 10, children: [
TextButton(
@@ -662,7 +692,7 @@ class _HealthAppState extends State {
if (p.value is AudiogramHealthValue) {
return ListTile(
title: Text("${p.typeString}: ${p.value}"),
- trailing: Text('${p.unitString}'),
+ trailing: Text(p.unitString),
subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'),
);
}
@@ -670,8 +700,8 @@ class _HealthAppState extends State {
return ListTile(
title: Text(
"${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.name}"),
- trailing: Text(
- '${(p.value as WorkoutHealthValue).workoutActivityType.name}'),
+ trailing:
+ Text((p.value as WorkoutHealthValue).workoutActivityType.name),
subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'),
);
}
@@ -686,46 +716,46 @@ class _HealthAppState extends State {
}
return ListTile(
title: Text("${p.typeString}: ${p.value}"),
- trailing: Text('${p.unitString}'),
+ trailing: Text(p.unitString),
subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'),
);
});
- Widget _contentNoData = const Text('No Data to show');
+ final Widget _contentNoData = const Text('No Data to show');
- Widget _contentNotFetched =
+ final 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."),
+ Text("Press 'Auth' to get permissions to access health data."),
+ Text("Press 'Fetch Dat' to get health data."),
+ Text("Press 'Add Data' to add some random health data."),
+ Text("Press 'Delete Data' to remove some random health data."),
]);
- Widget _authorized = const Text('Authorization granted!');
+ final Widget _authorized = const Text('Authorization granted!');
- Widget _authorizationNotGranted = const Column(
+ final Widget _authorizationNotGranted = const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
- const Text('Authorization not given.'),
- const Text(
+ Text('Authorization not given.'),
+ 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.'),
+ Text('For Apple Health check your permissions in Apple Health.'),
],
);
Widget _contentHealthConnectStatus = const Text(
'No status, click getHealthConnectSdkStatus to get the status.');
- Widget _dataAdded = const Text('Data points inserted successfully.');
+ final Widget _dataAdded = const Text('Data points inserted successfully.');
- Widget _dataDeleted = const Text('Data points deleted successfully.');
+ final Widget _dataDeleted = const Text('Data points deleted successfully.');
Widget get _stepsFetched => Text('Total number of steps: $_nofSteps.');
- Widget _dataNotAdded =
+ final Widget _dataNotAdded =
const Text('Failed to add data.\nDo you have permissions to add data?');
- Widget _dataNotDeleted = const Text('Failed to delete data');
+ final Widget _dataNotDeleted = const Text('Failed to delete data');
Widget get _content => switch (_state) {
AppState.DATA_READY => _contentDataReady,
diff --git a/packages/health/example/lib/util.dart b/packages/health/example/lib/util.dart
index 0a694b0a2..5c6f70be8 100644
--- a/packages/health/example/lib/util.dart
+++ b/packages/health/example/lib/util.dart
@@ -44,21 +44,24 @@ const List dataTypesIOS = [
HealthDataType.HEADACHE_MODERATE,
HealthDataType.HEADACHE_SEVERE,
HealthDataType.HEADACHE_UNSPECIFIED,
+ HealthDataType.LEAN_BODY_MASS,
// note that a phone cannot write these ECG-based types - only read them
- HealthDataType.ELECTROCARDIOGRAM,
- HealthDataType.HIGH_HEART_RATE_EVENT,
- HealthDataType.IRREGULAR_HEART_RATE_EVENT,
- HealthDataType.LOW_HEART_RATE_EVENT,
- HealthDataType.RESTING_HEART_RATE,
- HealthDataType.WALKING_HEART_RATE,
- HealthDataType.ATRIAL_FIBRILLATION_BURDEN,
+ // HealthDataType.ELECTROCARDIOGRAM,
+ // HealthDataType.HIGH_HEART_RATE_EVENT,
+ // HealthDataType.IRREGULAR_HEART_RATE_EVENT,
+ // HealthDataType.LOW_HEART_RATE_EVENT,
+ // HealthDataType.RESTING_HEART_RATE,
+ // HealthDataType.WALKING_HEART_RATE,
+ // HealthDataType.ATRIAL_FIBRILLATION_BURDEN,
HealthDataType.NUTRITION,
HealthDataType.GENDER,
HealthDataType.BLOOD_TYPE,
HealthDataType.BIRTH_DATE,
HealthDataType.MENSTRUATION_FLOW,
+ HealthDataType.WATER_TEMPERATURE,
+ HealthDataType.UNDERWATER_DEPTH,
];
/// List of data types available on Android.
@@ -76,6 +79,7 @@ const List dataTypesAndroid = [
HealthDataType.BODY_FAT_PERCENTAGE,
HealthDataType.HEIGHT,
HealthDataType.WEIGHT,
+ HealthDataType.LEAN_BODY_MASS,
// HealthDataType.BODY_MASS_INDEX,
HealthDataType.BODY_TEMPERATURE,
HealthDataType.HEART_RATE,
diff --git a/packages/health/example/pubspec.yaml b/packages/health/example/pubspec.yaml
index d472b3f03..031bcc6c2 100644
--- a/packages/health/example/pubspec.yaml
+++ b/packages/health/example/pubspec.yaml
@@ -11,7 +11,7 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
- permission_handler: ^10.2.0
+ permission_handler: ^11.3.1
carp_serializable: ^2.0.0 # polymorphic json serialization
health:
path: ../
diff --git a/packages/health/ios/Classes/SwiftHealthPlugin.swift b/packages/health/ios/Classes/SwiftHealthPlugin.swift
index d7af83e05..282ae9ff6 100644
--- a/packages/health/ios/Classes/SwiftHealthPlugin.swift
+++ b/packages/health/ios/Classes/SwiftHealthPlugin.swift
@@ -35,6 +35,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
let BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC"
let BLOOD_PRESSURE_SYSTOLIC = "BLOOD_PRESSURE_SYSTOLIC"
let BODY_FAT_PERCENTAGE = "BODY_FAT_PERCENTAGE"
+ let LEAN_BODY_MASS = "LEAN_BODY_MASS"
let BODY_MASS_INDEX = "BODY_MASS_INDEX"
let BODY_TEMPERATURE = "BODY_TEMPERATURE"
// Nutrition
@@ -161,6 +162,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
let GENDER = "GENDER"
let BLOOD_TYPE = "BLOOD_TYPE"
let MENSTRUATION_FLOW = "MENSTRUATION_FLOW"
+ let WATER_TEMPERATURE = "WATER_TEMPERATURE"
+ let UNDERWATER_DEPTH = "UNDERWATER_DEPTH"
// Health Unit types
@@ -339,7 +342,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
}
}
- result(false)
+ result(true)
}
func hasPermission(type: HKObjectType, access: Int) -> Bool? {
@@ -731,12 +734,15 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
let dataType = dataTypeLookUp(key: dataTypeKey)
- let predicate = HKQuery.predicateForSamples(
+ let samplePredicate = HKQuery.predicateForSamples(
withStart: dateFrom, end: dateTo, options: .strictStartDate)
+ let ownerPredicate = HKQuery.predicateForObjects(from: HKSource.default())
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let deleteQuery = HKSampleQuery(
- sampleType: dataType, predicate: predicate, limit: HKObjectQueryNoLimit,
+ sampleType: dataType,
+ predicate: NSCompoundPredicate(andPredicateWithSubpredicates: [samplePredicate, ownerPredicate]),
+ limit: HKObjectQueryNoLimit,
sortDescriptors: [sortDescriptor]
) { [self] x, samplesOrNil, error in
@@ -854,7 +860,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
"recording_method": (sample.metadata?[HKMetadataKeyWasUserEntered] as? Bool == true)
? RecordingMethod.manual.rawValue
: RecordingMethod.automatic.rawValue,
- "metadata": dataTypeKey == INSULIN_DELIVERY ? sample.metadata : nil
+ "metadata": dataTypeKey == INSULIN_DELIVERY ? sample.metadata : nil,
+ "dataUnitKey": unit?.unitString
]
}
DispatchQueue.main.async {
@@ -1171,33 +1178,36 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
predicate = NSCompoundPredicate(type: .and, subpredicates: [predicate, manualPredicate])
}
- let query = HKStatisticsQuery(
+ // TODO: [NOTE] Computational heavy
+ let query = HKStatisticsCollectionQuery(
quantityType: sampleType,
quantitySamplePredicate: predicate,
- options: .cumulativeSum
- ) { query, queryResult, error in
-
- guard let queryResult = queryResult else {
+ options: .cumulativeSum,
+ anchorDate: dateFrom,
+ intervalComponents: DateComponents(day: 1)
+ )
+ query.initialResultsHandler = { query, results, error in
+ guard let results = results else {
let error = error! as NSError
print("Error getting total steps in interval \(error.localizedDescription)")
-
+
DispatchQueue.main.async {
result(nil)
}
return
- }
-
- var steps = 0.0
-
- if let quantity = queryResult.sumQuantity() {
- let unit = HKUnit.count()
- steps = quantity.doubleValue(for: unit)
- }
-
- let totalSteps = Int(steps)
- DispatchQueue.main.async {
- result(totalSteps)
- }
+ }
+
+ var totalSteps = 0.0
+ results.enumerateStatistics(from: dateFrom, to: dateTo) { statistics, stop in
+ if let quantity = statistics.sumQuantity() {
+ let unit = HKUnit.count()
+ totalSteps += quantity.doubleValue(for: unit)
+ }
+ }
+
+ DispatchQueue.main.async {
+ result(Int(totalSteps))
+ }
}
HKHealthStore().execute(query)
@@ -1378,6 +1388,10 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
workoutActivityTypeMap["TAI_CHI"] = .taiChi
workoutActivityTypeMap["WRESTLING"] = .wrestling
workoutActivityTypeMap["OTHER"] = .other
+ if #available(iOS 17.0, *) {
+ workoutActivityTypeMap["UNDERWATER_DIVING"] = .underwaterDiving
+ }
+
nutritionList = [
DIETARY_ENERGY_CONSUMED, DIETARY_CARBS_CONSUMED, DIETARY_PROTEIN_CONSUMED,
DIETARY_FATS_CONSUMED, DIETARY_CAFFEINE, DIETARY_FIBER, DIETARY_SUGAR,
@@ -1409,6 +1423,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
forIdentifier: .bloodPressureSystolic)!
dataTypesDict[BODY_FAT_PERCENTAGE] = HKSampleType.quantityType(
forIdentifier: .bodyFatPercentage)!
+ dataTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)!
dataTypesDict[BODY_MASS_INDEX] = HKSampleType.quantityType(forIdentifier: .bodyMassIndex)!
dataTypesDict[BODY_TEMPERATURE] = HKSampleType.quantityType(forIdentifier: .bodyTemperature)!
@@ -1507,6 +1522,7 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
dataQuantityTypesDict[BLOOD_PRESSURE_DIASTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic)!
dataQuantityTypesDict[BLOOD_PRESSURE_SYSTOLIC] = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!
dataQuantityTypesDict[BODY_FAT_PERCENTAGE] = HKQuantityType.quantityType(forIdentifier: .bodyFatPercentage)!
+ dataQuantityTypesDict[LEAN_BODY_MASS] = HKSampleType.quantityType(forIdentifier: .leanBodyMass)!
dataQuantityTypesDict[BODY_MASS_INDEX] = HKQuantityType.quantityType(forIdentifier: .bodyMassIndex)!
dataQuantityTypesDict[BODY_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .bodyTemperature)!
@@ -1611,6 +1627,9 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
if #available(iOS 16.0, *) {
dataTypesDict[ATRIAL_FIBRILLATION_BURDEN] = HKQuantityType.quantityType(forIdentifier: .atrialFibrillationBurden)!
+
+ dataTypesDict[WATER_TEMPERATURE] = HKQuantityType.quantityType(forIdentifier: .waterTemperature)!
+ dataTypesDict[UNDERWATER_DEPTH] = HKQuantityType.quantityType(forIdentifier: .underwaterDepth)!
}
// Concatenate heart events, headache and health data types (both may be empty)
@@ -1768,6 +1787,8 @@ public class SwiftHealthPlugin: NSObject, FlutterPlugin {
return "mixedCardio"
case .handCycling:
return "handCycling"
+ case .underwaterDiving:
+ return "underwaterDiving"
default:
return "other"
}
diff --git a/packages/health/ios/health.podspec b/packages/health/ios/health.podspec
index 3a8d89370..96254602b 100644
--- a/packages/health/ios/health.podspec
+++ b/packages/health/ios/health.podspec
@@ -3,14 +3,14 @@
#
Pod::Spec.new do |s|
s.name = 'health'
- s.version = '1.0.4'
+ s.version = '12.0.0'
s.summary = 'Wrapper for Apple\'s HealthKit on iOS and Google\'s Health Connect on Android.'
s.description = <<-DESC
Wrapper for Apple's HealthKit on iOS and Google's Health Connect on Android.
DESC
s.homepage = 'https://pub.dev/packages/health'
s.license = { :file => '../LICENSE' }
- s.author = { 'Copenhagen Center for Health Technology' => 'cph.cachet@gmail.com' }
+ s.author = { 'Copenhagen Research Platform at DTU' => 'support@carp.dk' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
diff --git a/packages/health/lib/health.dart b/packages/health/lib/health.dart
index 1c960d54f..af94d80e7 100644
--- a/packages/health/lib/health.dart
+++ b/packages/health/lib/health.dart
@@ -1,4 +1,4 @@
-library health;
+library;
import 'dart:async';
import 'dart:collection';
diff --git a/packages/health/lib/health.g.dart b/packages/health/lib/health.g.dart
index fa4423446..70feb2de1 100644
--- a/packages/health/lib/health.g.dart
+++ b/packages/health/lib/health.g.dart
@@ -29,31 +29,23 @@ HealthDataPoint _$HealthDataPointFromJson(Map json) =>
metadata: json['metadata'] as Map?,
);
-Map _$HealthDataPointToJson(HealthDataPoint instance) {
- final val = {
- 'uuid': instance.uuid,
- 'value': instance.value.toJson(),
- 'type': _$HealthDataTypeEnumMap[instance.type]!,
- 'unit': _$HealthDataUnitEnumMap[instance.unit]!,
- '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) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('workoutSummary', instance.workoutSummary?.toJson());
- writeNotNull('metadata', instance.metadata);
- return val;
-}
+Map _$HealthDataPointToJson(HealthDataPoint instance) =>
+ {
+ 'uuid': instance.uuid,
+ 'value': instance.value.toJson(),
+ 'type': _$HealthDataTypeEnumMap[instance.type]!,
+ 'unit': _$HealthDataUnitEnumMap[instance.unit]!,
+ '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]!,
+ if (instance.workoutSummary?.toJson() case final value?)
+ 'workoutSummary': value,
+ if (instance.metadata case final value?) 'metadata': value,
+ };
const _$HealthDataTypeEnumMap = {
HealthDataType.ACTIVE_ENERGY_BURNED: 'ACTIVE_ENERGY_BURNED',
@@ -65,6 +57,7 @@ const _$HealthDataTypeEnumMap = {
HealthDataType.BLOOD_PRESSURE_DIASTOLIC: 'BLOOD_PRESSURE_DIASTOLIC',
HealthDataType.BLOOD_PRESSURE_SYSTOLIC: 'BLOOD_PRESSURE_SYSTOLIC',
HealthDataType.BODY_FAT_PERCENTAGE: 'BODY_FAT_PERCENTAGE',
+ HealthDataType.LEAN_BODY_MASS: 'LEAN_BODY_MASS',
HealthDataType.BODY_MASS_INDEX: 'BODY_MASS_INDEX',
HealthDataType.BODY_TEMPERATURE: 'BODY_TEMPERATURE',
HealthDataType.BODY_WATER_MASS: 'BODY_WATER_MASS',
@@ -148,6 +141,8 @@ const _$HealthDataTypeEnumMap = {
HealthDataType.BIRTH_DATE: 'BIRTH_DATE',
HealthDataType.BLOOD_TYPE: 'BLOOD_TYPE',
HealthDataType.MENSTRUATION_FLOW: 'MENSTRUATION_FLOW',
+ HealthDataType.WATER_TEMPERATURE: 'WATER_TEMPERATURE',
+ HealthDataType.UNDERWATER_DEPTH: 'UNDERWATER_DEPTH',
HealthDataType.HIGH_HEART_RATE_EVENT: 'HIGH_HEART_RATE_EVENT',
HealthDataType.LOW_HEART_RATE_EVENT: 'LOW_HEART_RATE_EVENT',
HealthDataType.IRREGULAR_HEART_RATE_EVENT: 'IRREGULAR_HEART_RATE_EVENT',
@@ -223,37 +218,21 @@ const _$RecordingMethodEnumMap = {
HealthValue _$HealthValueFromJson(Map json) =>
HealthValue()..$type = json['__type'] as String?;
-Map _$HealthValueToJson(HealthValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- return val;
-}
+Map _$HealthValueToJson(HealthValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ };
NumericHealthValue _$NumericHealthValueFromJson(Map json) =>
NumericHealthValue(
numericValue: json['numericValue'] as num,
)..$type = json['__type'] as String?;
-Map _$NumericHealthValueToJson(NumericHealthValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- val['numericValue'] = instance.numericValue;
- return val;
-}
+Map _$NumericHealthValueToJson(NumericHealthValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ 'numericValue': instance.numericValue,
+ };
AudiogramHealthValue _$AudiogramHealthValueFromJson(
Map json) =>
@@ -269,21 +248,13 @@ AudiogramHealthValue _$AudiogramHealthValueFromJson(
)..$type = json['__type'] as String?;
Map _$AudiogramHealthValueToJson(
- AudiogramHealthValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- val['frequencies'] = instance.frequencies;
- val['leftEarSensitivities'] = instance.leftEarSensitivities;
- val['rightEarSensitivities'] = instance.rightEarSensitivities;
- return val;
-}
+ AudiogramHealthValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ 'frequencies': instance.frequencies,
+ 'leftEarSensitivities': instance.leftEarSensitivities,
+ 'rightEarSensitivities': instance.rightEarSensitivities,
+ };
WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) =>
WorkoutHealthValue(
@@ -300,29 +271,23 @@ WorkoutHealthValue _$WorkoutHealthValueFromJson(Map json) =>
$enumDecodeNullable(_$HealthDataUnitEnumMap, json['totalStepsUnit']),
)..$type = json['__type'] as String?;
-Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- val['workoutActivityType'] =
- _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!;
- writeNotNull('totalEnergyBurned', instance.totalEnergyBurned);
- writeNotNull('totalEnergyBurnedUnit',
- _$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit]);
- writeNotNull('totalDistance', instance.totalDistance);
- writeNotNull(
- 'totalDistanceUnit', _$HealthDataUnitEnumMap[instance.totalDistanceUnit]);
- writeNotNull('totalSteps', instance.totalSteps);
- writeNotNull(
- 'totalStepsUnit', _$HealthDataUnitEnumMap[instance.totalStepsUnit]);
- return val;
-}
+Map _$WorkoutHealthValueToJson(WorkoutHealthValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ 'workoutActivityType':
+ _$HealthWorkoutActivityTypeEnumMap[instance.workoutActivityType]!,
+ if (instance.totalEnergyBurned case final value?)
+ 'totalEnergyBurned': value,
+ if (_$HealthDataUnitEnumMap[instance.totalEnergyBurnedUnit]
+ case final value?)
+ 'totalEnergyBurnedUnit': value,
+ if (instance.totalDistance case final value?) 'totalDistance': value,
+ if (_$HealthDataUnitEnumMap[instance.totalDistanceUnit] case final value?)
+ 'totalDistanceUnit': value,
+ if (instance.totalSteps case final value?) 'totalSteps': value,
+ if (_$HealthDataUnitEnumMap[instance.totalStepsUnit] case final value?)
+ 'totalStepsUnit': value,
+ };
const _$HealthWorkoutActivityTypeEnumMap = {
HealthWorkoutActivityType.AMERICAN_FOOTBALL: 'AMERICAN_FOOTBALL',
@@ -406,6 +371,7 @@ const _$HealthWorkoutActivityTypeEnumMap = {
HealthWorkoutActivityType.WHEELCHAIR_RUN_PACE: 'WHEELCHAIR_RUN_PACE',
HealthWorkoutActivityType.WHEELCHAIR_WALK_PACE: 'WHEELCHAIR_WALK_PACE',
HealthWorkoutActivityType.WRESTLING: 'WRESTLING',
+ HealthWorkoutActivityType.UNDERWATER_DIVING: 'UNDERWATER_DIVING',
HealthWorkoutActivityType.BIKING_STATIONARY: 'BIKING_STATIONARY',
HealthWorkoutActivityType.CALISTHENICS: 'CALISTHENICS',
HealthWorkoutActivityType.DANCING: 'DANCING',
@@ -443,23 +409,18 @@ ElectrocardiogramHealthValue _$ElectrocardiogramHealthValueFromJson(
)..$type = json['__type'] as String?;
Map _$ElectrocardiogramHealthValueToJson(
- ElectrocardiogramHealthValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- val['voltageValues'] = instance.voltageValues.map((e) => e.toJson()).toList();
- writeNotNull('averageHeartRate', instance.averageHeartRate);
- writeNotNull('samplingFrequency', instance.samplingFrequency);
- writeNotNull('classification',
- _$ElectrocardiogramClassificationEnumMap[instance.classification]);
- return val;
-}
+ ElectrocardiogramHealthValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ 'voltageValues': instance.voltageValues.map((e) => e.toJson()).toList(),
+ if (instance.averageHeartRate case final value?)
+ 'averageHeartRate': value,
+ if (instance.samplingFrequency case final value?)
+ 'samplingFrequency': value,
+ if (_$ElectrocardiogramClassificationEnumMap[instance.classification]
+ case final value?)
+ 'classification': value,
+ };
const _$ElectrocardiogramClassificationEnumMap = {
ElectrocardiogramClassification.NOT_SET: 'NOT_SET',
@@ -483,20 +444,12 @@ ElectrocardiogramVoltageValue _$ElectrocardiogramVoltageValueFromJson(
)..$type = json['__type'] as String?;
Map _$ElectrocardiogramVoltageValueToJson(
- ElectrocardiogramVoltageValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- val['voltage'] = instance.voltage;
- val['timeSinceSampleStart'] = instance.timeSinceSampleStart;
- return val;
-}
+ ElectrocardiogramVoltageValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ 'voltage': instance.voltage,
+ 'timeSinceSampleStart': instance.timeSinceSampleStart,
+ };
InsulinDeliveryHealthValue _$InsulinDeliveryHealthValueFromJson(
Map json) =>
@@ -506,20 +459,12 @@ InsulinDeliveryHealthValue _$InsulinDeliveryHealthValueFromJson(
)..$type = json['__type'] as String?;
Map _$InsulinDeliveryHealthValueToJson(
- InsulinDeliveryHealthValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- val['units'] = instance.units;
- val['reason'] = _$InsulinDeliveryReasonEnumMap[instance.reason]!;
- return val;
-}
+ InsulinDeliveryHealthValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ 'units': instance.units,
+ 'reason': _$InsulinDeliveryReasonEnumMap[instance.reason]!,
+ };
const _$InsulinDeliveryReasonEnumMap = {
InsulinDeliveryReason.NOT_SET: 'NOT_SET',
@@ -577,62 +522,58 @@ NutritionHealthValue _$NutritionHealthValueFromJson(
)..$type = json['__type'] as String?;
Map _$NutritionHealthValueToJson(
- NutritionHealthValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- writeNotNull('name', instance.name);
- 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('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('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);
- writeNotNull('magnesium', instance.magnesium);
- writeNotNull('manganese', instance.manganese);
- writeNotNull('molybdenum', instance.molybdenum);
- writeNotNull('phosphorus', instance.phosphorus);
- writeNotNull('potassium', instance.potassium);
- writeNotNull('selenium', instance.selenium);
- writeNotNull('sodium', instance.sodium);
- writeNotNull('sugar', instance.sugar);
- writeNotNull('water', instance.water);
- writeNotNull('zinc', instance.zinc);
- return val;
-}
+ NutritionHealthValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ if (instance.name case final value?) 'name': value,
+ if (instance.mealType case final value?) 'mealType': value,
+ if (instance.calories case final value?) 'calories': value,
+ if (instance.protein case final value?) 'protein': value,
+ if (instance.fat case final value?) 'fat': value,
+ if (instance.carbs case final value?) 'carbs': value,
+ if (instance.caffeine case final value?) 'caffeine': value,
+ if (instance.vitaminA case final value?) 'vitaminA': value,
+ if (instance.b1Thiamine case final value?) 'b1Thiamine': value,
+ if (instance.b2Riboflavin case final value?) 'b2Riboflavin': value,
+ if (instance.b3Niacin case final value?) 'b3Niacin': value,
+ if (instance.b5PantothenicAcid case final value?)
+ 'b5PantothenicAcid': value,
+ if (instance.b6Pyridoxine case final value?) 'b6Pyridoxine': value,
+ if (instance.b7Biotin case final value?) 'b7Biotin': value,
+ if (instance.b9Folate case final value?) 'b9Folate': value,
+ if (instance.b12Cobalamin case final value?) 'b12Cobalamin': value,
+ if (instance.vitaminC case final value?) 'vitaminC': value,
+ if (instance.vitaminD case final value?) 'vitaminD': value,
+ if (instance.vitaminE case final value?) 'vitaminE': value,
+ if (instance.vitaminK case final value?) 'vitaminK': value,
+ if (instance.calcium case final value?) 'calcium': value,
+ if (instance.chloride case final value?) 'chloride': value,
+ if (instance.cholesterol case final value?) 'cholesterol': value,
+ if (instance.choline case final value?) 'choline': value,
+ if (instance.chromium case final value?) 'chromium': value,
+ if (instance.copper case final value?) 'copper': value,
+ if (instance.fatUnsaturated case final value?) 'fatUnsaturated': value,
+ if (instance.fatMonounsaturated case final value?)
+ 'fatMonounsaturated': value,
+ if (instance.fatPolyunsaturated case final value?)
+ 'fatPolyunsaturated': value,
+ if (instance.fatSaturated case final value?) 'fatSaturated': value,
+ if (instance.fatTransMonoenoic case final value?)
+ 'fatTransMonoenoic': value,
+ if (instance.fiber case final value?) 'fiber': value,
+ if (instance.iodine case final value?) 'iodine': value,
+ if (instance.iron case final value?) 'iron': value,
+ if (instance.magnesium case final value?) 'magnesium': value,
+ if (instance.manganese case final value?) 'manganese': value,
+ if (instance.molybdenum case final value?) 'molybdenum': value,
+ if (instance.phosphorus case final value?) 'phosphorus': value,
+ if (instance.potassium case final value?) 'potassium': value,
+ if (instance.selenium case final value?) 'selenium': value,
+ if (instance.sodium case final value?) 'sodium': value,
+ if (instance.sugar case final value?) 'sugar': value,
+ if (instance.water case final value?) 'water': value,
+ if (instance.zinc case final value?) 'zinc': value,
+ };
MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson(
Map json) =>
@@ -644,22 +585,15 @@ MenstruationFlowHealthValue _$MenstruationFlowHealthValueFromJson(
)..$type = json['__type'] as String?;
Map _$MenstruationFlowHealthValueToJson(
- MenstruationFlowHealthValue instance) {
- final val = {};
-
- void writeNotNull(String key, dynamic value) {
- if (value != null) {
- val[key] = value;
- }
- }
-
- writeNotNull('__type', instance.$type);
- writeNotNull('flow', _$MenstrualFlowEnumMap[instance.flow]);
- writeNotNull('isStartOfCycle', instance.isStartOfCycle);
- writeNotNull('wasUserEntered', instance.wasUserEntered);
- val['dateTime'] = instance.dateTime.toIso8601String();
- return val;
-}
+ MenstruationFlowHealthValue instance) =>
+ {
+ if (instance.$type case final value?) '__type': value,
+ if (_$MenstrualFlowEnumMap[instance.flow] case final value?)
+ 'flow': value,
+ if (instance.isStartOfCycle case final value?) 'isStartOfCycle': value,
+ if (instance.wasUserEntered case final value?) 'wasUserEntered': value,
+ 'dateTime': instance.dateTime.toIso8601String(),
+ };
const _$MenstrualFlowEnumMap = {
MenstrualFlow.unspecified: 'unspecified',
diff --git a/packages/health/lib/health.json.dart b/packages/health/lib/health.json.dart
index 351098a28..2162869da 100644
--- a/packages/health/lib/health.json.dart
+++ b/packages/health/lib/health.json.dart
@@ -10,6 +10,7 @@ void _registerFromJsonFunctions() {
FromJsonFactory().registerAll([
HealthValue(),
NumericHealthValue(numericValue: 12),
+ WorkoutHealthValue(workoutActivityType: HealthWorkoutActivityType.RUNNING),
AudiogramHealthValue(
frequencies: [],
leftEarSensitivities: [],
diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart
index 8868a519b..cc62e3ac7 100644
--- a/packages/health/lib/src/health_plugin.dart
+++ b/packages/health/lib/src/health_plugin.dart
@@ -1,8 +1,12 @@
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.
+///
+/// Use this class to get an instance of the Health plugin, like this:
+///
+/// final health = Health();
+///
+/// The plugin must be configured using the [configure] method before used.
///
/// Overall, the plugin supports:
///
@@ -34,20 +38,18 @@ part of '../health.dart';
/// or getter methods. Otherwise, the plugin will throw an exception.
class Health {
static const MethodChannel _channel = MethodChannel('flutter_health');
- static final _instance = Health._();
String? _deviceId;
- final _deviceInfo = DeviceInfoPlugin();
+ final DeviceInfoPlugin _deviceInfo;
HealthConnectSdkStatus _healthConnectSdkStatus =
HealthConnectSdkStatus.sdkUnavailable;
- Health._() {
+ /// Get an instance of the health plugin.
+ Health({DeviceInfoPlugin? deviceInfo})
+ : _deviceInfo = deviceInfo ?? DeviceInfoPlugin() {
_registerFromJsonFunctions();
}
- /// The singleton [Health] instance.
- factory Health() => _instance;
-
/// The latest status on availability of Health Connect SDK on this phone.
HealthConnectSdkStatus get healthConnectSdkStatus => _healthConnectSdkStatus;
@@ -1104,7 +1106,7 @@ class Health {
HealthDataType.SLEEP_IN_BED => 0,
HealthDataType.SLEEP_ASLEEP => 1,
HealthDataType.SLEEP_AWAKE => 2,
- HealthDataType.SLEEP_ASLEEP => 3,
+ HealthDataType.SLEEP_LIGHT => 3,
HealthDataType.SLEEP_DEEP => 4,
HealthDataType.SLEEP_REM => 5,
HealthDataType.HEADACHE_UNSPECIFIED => 0,
@@ -1257,6 +1259,7 @@ class Health {
HealthWorkoutActivityType.YOGA,
HealthWorkoutActivityType.SWIMMING_OPEN_WATER,
HealthWorkoutActivityType.SWIMMING_POOL,
+ HealthWorkoutActivityType.UNDERWATER_DIVING,
}.contains(type);
}
diff --git a/packages/health/lib/src/health_value_types.dart b/packages/health/lib/src/health_value_types.dart
index 4b88adc50..af13e1892 100644
--- a/packages/health/lib/src/health_value_types.dart
+++ b/packages/health/lib/src/health_value_types.dart
@@ -608,8 +608,6 @@ class NutritionHealthValue extends HealthValue {
@override
Map toJson() => _$NutritionHealthValueToJson(this);
- static double? _toDoubleOrNull(num? value) => value?.toDouble();
-
/// Create a [NutritionHealthValue] based on a health data point from native data format.
factory NutritionHealthValue.fromHealthDataPoint(dynamic dataPoint) {
dataPoint = dataPoint as Map