diff --git a/README.md b/README.md index 3f5e02058..d08af73a1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CARP Flutter plugins -This repo contains the source code for Flutter first-party plugins developed by developers at the [Copenhagen Research Platform (CARP)](http://www.carp.dk/) at The Technical University of Denmark. +This repo contains the source code for Flutter first-party plugins maintained by the [Copenhagen Research Platform (CARP)](http://www.carp.dk/) team at the Technical University of Denmark. Check the `packages` directory for all plugins. Flutter plugins enable access to platform-specific APIs using a platform channel. @@ -10,7 +10,7 @@ For more information about plugins and how to use them, see ## Plugins These are the available plugins in this repository. -| Plugin | Description | Android | iOS | http://pub.dev/ | +| Plugin | Description | Android | iOS | pub.dev | |--------|-------------|:-------:|:---:|:---------:| | [screen_state](./packages/screen_state) | Track screen state changes | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/screen_state.svg)](https://pub.dartlang.org/packages/screen_state) | | [light](./packages/light) | Track light sensor readings | ✔️ | ❌ | [![pub package](https://img.shields.io/pub/v/light.svg)](https://pub.dartlang.org/packages/light) | @@ -36,18 +36,8 @@ Please check existing issues and file any new issues, bugs, or feature requests ## Contributing As part of the open-source Flutter ecosystem, we would welcome any help in maintaining and enhancing these plugins. -We (i.e., CACHET) have limited resources for maintaining these plugins and we rely on **your** help in this. -We welcome any contribution -- from small error corrections in the documentation, to bug fixes, to large features enhacements, or even new features in a plugin. -If you wish to contribute to any of the plugins in this repo, -please review our [contribution guide](https://github.com/cph-cachet/flutter-plugins/CONTRIBUTING.md), -and send a [pull request](https://github.com/cph-cachet/flutter-plugins/pulls). - - -In general, if you wish to contribute a new plugin to the Flutter ecosystem, please -see the documentation for [developing packages](https://flutter.io/developing-packages/) and -[platform channels](https://flutter.io/platform-channels/). You can store -your plugin source code in any GitHub repository (the present repo is only -intended for plugins developed by the core CARP team). Once your plugin -is ready you can [publish](https://flutter.io/developing-packages/#publish) -to the [pub repository](https://pub.dartlang.org/). +We (i.e., the CARP team) have limited resources for maintaining these plugins, and we rely on **your** help in this. +We welcome any contribution - from small error corrections in the documentation, to bug fixes, to large feature enhancements, or even new features in a plugin. +If you wish to contribute to any of the plugins in this repo, please review our [contribution guide](https://github.com/cph-cachet/flutter-plugins/CONTRIBUTING.md) and send a [pull request](https://github.com/cph-cachet/flutter-plugins/pulls). + diff --git a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt index 976a9bf15..8ce8843d0 100644 --- a/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt +++ b/packages/health/android/src/main/kotlin/cachet/plugins/health/HealthDataWriter.kt @@ -5,6 +5,7 @@ import androidx.health.connect.client.HealthConnectClient import androidx.health.connect.client.records.* import androidx.health.connect.client.records.metadata.Device import androidx.health.connect.client.records.metadata.Metadata +import androidx.health.connect.client.response.InsertRecordsResponse import androidx.health.connect.client.units.* import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel.Result @@ -123,17 +124,31 @@ class HealthDataWriter( val record = createRecord(type, startTime, endTime, value, metadata) if (record == null) { - result.success(false) + result.success("") return } scope.launch { try { - healthConnectClient.insertRecords(listOf(record)) - result.success(true) + // Insert records into Health Connect + val insertResponse: InsertRecordsResponse = healthConnectClient.insertRecords(listOf(record)) + + // Extract UUID from the first inserted record + val insertedUUID = insertResponse.recordIdsList.firstOrNull() ?: "" + + if (insertedUUID.isEmpty()) { + Log.e("FLUTTER_HEALTH::ERROR", "UUID is empty! No records were inserted.") + } else { + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Workout $insertedUUID was successfully added!" + ) + } + + result.success(insertedUUID) } catch (e: Exception) { Log.e("FLUTTER_HEALTH::ERROR", "Error writing $type: ${e.message}") - result.success(false) + result.success("") } } } @@ -161,8 +176,11 @@ class HealthDataWriter( val workoutMetadata = buildMetadata(recordingMethod = recordingMethod, deviceType = deviceType) if (!HealthConstants.workoutTypeMap.containsKey(type)) { - result.success(false) - Log.w("FLUTTER_HEALTH::ERROR", "[Health Connect] Workout type not supported") + result.success("") + Log.w( + "FLUTTER_HEALTH::ERROR", + "[Health Connect] Workout type not supported" + ) return } @@ -213,10 +231,23 @@ class HealthDataWriter( ), ) } + + // Insert records into Health Connect + val insertResponse: InsertRecordsResponse = healthConnectClient.insertRecords(list) + + // Extract UUID from the first inserted record + val insertedUUID = insertResponse.recordIdsList.firstOrNull() ?: "" + + if (insertedUUID.isEmpty()) { + Log.e("FLUTTER_HEALTH::ERROR", "UUID is empty! No records were inserted.") + } - healthConnectClient.insertRecords(list) - result.success(true) - Log.i("FLUTTER_HEALTH::SUCCESS", "[Health Connect] Workout was successfully added!") + Log.i( + "FLUTTER_HEALTH::SUCCESS", + "[Health Connect] Workout $insertedUUID was successfully added!" + ) + + result.success(insertedUUID) } catch (e: Exception) { Log.w( "FLUTTER_HEALTH::ERROR", @@ -224,7 +255,7 @@ class HealthDataWriter( ) Log.w("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error") Log.w("FLUTTER_HEALTH::ERROR", e.stackTrace.toString()) - result.success(false) + result.success("") } } } diff --git a/packages/health/example/lib/main.dart b/packages/health/example/lib/main.dart index ffaeec3bf..15cba0bcd 100644 --- a/packages/health/example/lib/main.dart +++ b/packages/health/example/lib/main.dart @@ -47,8 +47,8 @@ class HealthAppState extends State { List get types => (Platform.isAndroid) ? dataTypesAndroid : (Platform.isIOS) - ? dataTypesIOS - : []; + ? dataTypesIOS + : []; // // Or specify specific types // static final types = [ @@ -71,24 +71,26 @@ class HealthAppState extends State { // Or both READ and WRITE List get permissions => types - .map((type) => - // can only request READ permissions to the following list of types on iOS - [ - HealthDataType.GENDER, - HealthDataType.BLOOD_TYPE, - HealthDataType.BIRTH_DATE, - HealthDataType.APPLE_MOVE_TIME, - HealthDataType.APPLE_STAND_HOUR, - HealthDataType.APPLE_STAND_TIME, - HealthDataType.WALKING_HEART_RATE, - HealthDataType.ELECTROCARDIOGRAM, - HealthDataType.HIGH_HEART_RATE_EVENT, - HealthDataType.LOW_HEART_RATE_EVENT, - HealthDataType.IRREGULAR_HEART_RATE_EVENT, - HealthDataType.EXERCISE_TIME, - ].contains(type) - ? HealthDataAccess.READ - : HealthDataAccess.READ_WRITE) + .map( + (type) => + // can only request READ permissions to the following list of types on iOS + [ + HealthDataType.GENDER, + HealthDataType.BLOOD_TYPE, + HealthDataType.BIRTH_DATE, + HealthDataType.APPLE_MOVE_TIME, + HealthDataType.APPLE_STAND_HOUR, + HealthDataType.APPLE_STAND_TIME, + HealthDataType.WALKING_HEART_RATE, + HealthDataType.ELECTROCARDIOGRAM, + HealthDataType.HIGH_HEART_RATE_EVENT, + HealthDataType.LOW_HEART_RATE_EVENT, + HealthDataType.IRREGULAR_HEART_RATE_EVENT, + HealthDataType.EXERCISE_TIME, + ].contains(type) + ? HealthDataAccess.READ + : HealthDataAccess.READ_WRITE, + ) .toList(); @override @@ -115,8 +117,10 @@ class HealthAppState extends State { await Permission.location.request(); // Check if we have health permissions - bool? hasPermissions = - await health.hasPermissions(types, permissions: permissions); + bool? hasPermissions = 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. @@ -126,8 +130,10 @@ 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, + ); // request access to read historic data await health.requestHealthDataHistoryAuthorization(); @@ -139,8 +145,11 @@ class HealthAppState extends State { } } - setState(() => _state = - (authorized) ? AppState.AUTHORIZED : AppState.AUTH_NOT_GRANTED); + setState( + () => _state = (authorized) + ? AppState.AUTHORIZED + : AppState.AUTH_NOT_GRANTED, + ); } /// Gets the Health Connect status on Android. @@ -150,8 +159,9 @@ class HealthAppState extends State { final status = await health.getHealthConnectSdkStatus(); setState(() { - _contentHealthConnectStatus = - Text('Health Connect Status: ${status?.name.toUpperCase()}'); + _contentHealthConnectStatus = Text( + 'Health Connect Status: ${status?.name.toUpperCase()}', + ); _state = AppState.HEALTH_CONNECT_STATUS; }); } @@ -176,15 +186,18 @@ class HealthAppState extends State { recordingMethodsToFilter: recordingMethodsToFilter, ); - debugPrint('Total number of data points: ${healthData.length}. ' - '${healthData.length > 100 ? 'Only showing the first 100.' : ''}'); + debugPrint( + 'Total number of data points: ${healthData.length}. ' + '${healthData.length > 100 ? 'Only showing the first 100.' : ''}', + ); // sort the data points by date healthData.sort((a, b) => b.dateTo.compareTo(a.dateTo)); // save all the new data points (only the first 100) _healthDataList.addAll( - (healthData.length < 100) ? healthData : healthData.sublist(0, 100)); + (healthData.length < 100) ? healthData : healthData.sublist(0, 100), + ); } catch (error) { debugPrint("Exception in getHealthDataFromTypes: $error"); } @@ -224,6 +237,45 @@ class HealthAppState extends State { } } + // Add single steps data (health data) + Future addSingleHealthData() async { + final now = DateTime.now(); + final earlier = now.subtract(const Duration(minutes: 20)); + + final healthDataPoint = await health.writeHealthData( + value: 2130, + type: HealthDataType.STEPS, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ); + + bool success = healthDataPoint != null; + setState(() { + _state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED; + }); + } + + // Add single running data (workout data) + Future addSingleWorkoutData() async { + final now = DateTime.now(); + final earlier = now.subtract(const Duration(minutes: 20)); + + final healthDataPoint = await health.writeWorkoutData( + activityType: HealthWorkoutActivityType.RUNNING, + title: "New RUNNING activity", + start: earlier, + end: now, + totalDistance: 2430, + totalEnergyBurned: 400, + ); + + bool success = healthDataPoint != null; + setState(() { + _state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED; + }); + } + /// Add some random health data. /// Note that you should ensure that you have permissions to add the /// following data types. @@ -238,83 +290,125 @@ class HealthAppState extends State { bool success = true; // misc. health data examples using the writeHealthData() method - success &= await health.writeHealthData( - value: 1.925, - type: HealthDataType.HEIGHT, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); - success &= await health.writeHealthData( - value: 90, - type: HealthDataType.WEIGHT, - startTime: now, - recordingMethod: RecordingMethod.manual); - success &= await health.writeHealthData( - value: 90, - type: HealthDataType.HEART_RATE, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); - success &= await health.writeHealthData( - value: 90, - type: HealthDataType.STEPS, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); - success &= await health.writeHealthData( - value: 200, - type: HealthDataType.ACTIVE_ENERGY_BURNED, - startTime: earlier, - endTime: now, - clientRecordId: "uniqueID1234", - clientRecordVersion: 1); - success &= await health.writeHealthData( - value: 70, - type: HealthDataType.HEART_RATE, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 37, - type: HealthDataType.BODY_TEMPERATURE, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 105, - type: HealthDataType.BLOOD_GLUCOSE, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 1.8, - type: HealthDataType.WATER, - startTime: earlier, - endTime: now); + success &= + await health.writeHealthData( + value: 1.925, + type: HealthDataType.HEIGHT, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; + success &= + await health.writeHealthData( + value: 90, + type: HealthDataType.WEIGHT, + startTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; + success &= + await health.writeHealthData( + value: 90, + type: HealthDataType.HEART_RATE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; + success &= + await health.writeHealthData( + value: 90, + type: HealthDataType.STEPS, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; + success &= + await health.writeHealthData( + value: 200, + type: HealthDataType.ACTIVE_ENERGY_BURNED, + startTime: earlier, + endTime: now, + clientRecordId: "uniqueID1234", + clientRecordVersion: 1, + ) != + null; + success &= + await health.writeHealthData( + value: 70, + type: HealthDataType.HEART_RATE, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + value: 37, + type: HealthDataType.BODY_TEMPERATURE, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + value: 105, + type: HealthDataType.BLOOD_GLUCOSE, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + value: 1.8, + type: HealthDataType.WATER, + startTime: earlier, + endTime: now, + ) != + null; // different types of sleep - success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_REM, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_ASLEEP, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 0.0, - type: HealthDataType.SLEEP_AWAKE, - startTime: earlier, - endTime: now); - 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); + success &= + await health.writeHealthData( + value: 0.0, + type: HealthDataType.SLEEP_REM, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + value: 0.0, + type: HealthDataType.SLEEP_ASLEEP, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + value: 0.0, + type: HealthDataType.SLEEP_AWAKE, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + value: 0.0, + type: HealthDataType.SLEEP_DEEP, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + value: 22, + type: HealthDataType.LEAN_BODY_MASS, + startTime: earlier, + endTime: now, + ) != + null; // specialized write methods success &= await health.writeBloodOxygen( @@ -322,69 +416,73 @@ class HealthAppState extends State { startTime: earlier, endTime: now, ); - 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)), - end: now, - totalDistance: 2430, - totalEnergyBurned: 400, - ); + 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)), + end: now, + totalDistance: 2430, + totalEnergyBurned: 400, + ) != + null; success &= await health.writeBloodPressure( - systolic: 90, - diastolic: 80, - startTime: now, - clientRecordId: "uniqueID1234", - clientRecordVersion: 2); + systolic: 90, + diastolic: 80, + startTime: now, + clientRecordId: "uniqueID1234", + clientRecordVersion: 2, + ); success &= await health.writeMeal( - mealType: MealType.SNACK, - clientRecordId: "uniqueID1234", - clientRecordVersion: 1.4, - startTime: earlier, - endTime: now, - caloriesConsumed: 1000, - carbohydrates: 50, - protein: 25, - fatTotal: 50, - name: "Banana", - caffeine: 0.002, - vitaminA: 0.001, - vitaminC: 0.002, - vitaminD: 0.003, - vitaminE: 0.004, - vitaminK: 0.005, - b1Thiamin: 0.006, - b2Riboflavin: 0.007, - b3Niacin: 0.008, - b5PantothenicAcid: 0.009, - b6Pyridoxine: 0.010, - b7Biotin: 0.011, - b9Folate: 0.012, - b12Cobalamin: 0.013, - calcium: 0.015, - copper: 0.016, - iodine: 0.017, - iron: 0.018, - magnesium: 0.019, - manganese: 0.020, - phosphorus: 0.021, - potassium: 0.022, - selenium: 0.023, - sodium: 0.024, - zinc: 0.025, - water: 0.026, - molybdenum: 0.027, - chloride: 0.028, - chromium: 0.029, - cholesterol: 0.030, - fiber: 0.031, - fatMonounsaturated: 0.032, - fatPolyunsaturated: 0.033, - fatUnsaturated: 0.065, - fatTransMonoenoic: 0.65, - fatSaturated: 066, - sugar: 0.067, - recordingMethod: RecordingMethod.manual); + mealType: MealType.SNACK, + clientRecordId: "uniqueID1234", + clientRecordVersion: 1.4, + startTime: earlier, + endTime: now, + caloriesConsumed: 1000, + carbohydrates: 50, + protein: 25, + fatTotal: 50, + name: "Banana", + caffeine: 0.002, + vitaminA: 0.001, + vitaminC: 0.002, + vitaminD: 0.003, + vitaminE: 0.004, + vitaminK: 0.005, + b1Thiamin: 0.006, + b2Riboflavin: 0.007, + b3Niacin: 0.008, + b5PantothenicAcid: 0.009, + b6Pyridoxine: 0.010, + b7Biotin: 0.011, + b9Folate: 0.012, + b12Cobalamin: 0.013, + calcium: 0.015, + copper: 0.016, + iodine: 0.017, + iron: 0.018, + magnesium: 0.019, + manganese: 0.020, + phosphorus: 0.021, + potassium: 0.022, + selenium: 0.023, + sodium: 0.024, + zinc: 0.025, + water: 0.026, + molybdenum: 0.027, + chloride: 0.028, + chromium: 0.029, + cholesterol: 0.030, + fiber: 0.031, + fatMonounsaturated: 0.032, + fatPolyunsaturated: 0.033, + fatUnsaturated: 0.065, + fatTransMonoenoic: 0.65, + fatSaturated: 066, + sugar: 0.067, + recordingMethod: RecordingMethod.manual, + ); // Store an Audiogram - only available on iOS // const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0]; @@ -411,62 +509,89 @@ class HealthAppState extends State { if (Platform.isIOS) { success &= await health.writeInsulinDelivery( - 5, InsulinDeliveryReason.BOLUS, earlier, now); - success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, - startTime: earlier, - endTime: now); - success &= await health.writeHealthData( - value: 1.5, // 1.5 m/s (typical walking speed) - type: HealthDataType.WALKING_SPEED, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + 5, + InsulinDeliveryReason.BOLUS, + earlier, + now, + ); + success &= + await health.writeHealthData( + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_SDNN, + startTime: earlier, + endTime: now, + ) != + null; + success &= + await health.writeHealthData( + value: 1.5, // 1.5 m/s (typical walking speed) + type: HealthDataType.WALKING_SPEED, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; } else { - success &= await health.writeHealthData( - value: 2.0, // 2.0 m/s (typical jogging speed) - type: HealthDataType.SPEED, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); - success &= await health.writeHealthData( - value: 30, - type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, - startTime: earlier, - endTime: now); + success &= + await health.writeHealthData( + value: 2.0, // 2.0 m/s (typical jogging speed) + type: HealthDataType.SPEED, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; + success &= + await health.writeHealthData( + value: 30, + type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD, + startTime: earlier, + endTime: now, + ) != + null; // Mindfulness value should be counted based on start and end time - success &= await health.writeHealthData( - value: 10, - type: HealthDataType.MINDFULNESS, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.automatic, - ); + success &= + await health.writeHealthData( + value: 10, + type: HealthDataType.MINDFULNESS, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.automatic, + ) != + null; } // Available on iOS or 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); - success &= await health.writeHealthData( - value: 4.3, - type: HealthDataType.UV_INDEX, - startTime: earlier, - endTime: now, - recordingMethod: RecordingMethod.manual); + success &= + await health.writeHealthData( + value: 22, + type: HealthDataType.WATER_TEMPERATURE, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; + + success &= + await health.writeHealthData( + value: 55, + type: HealthDataType.UNDERWATER_DEPTH, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; + success &= + await health.writeHealthData( + value: 4.3, + type: HealthDataType.UV_INDEX, + startTime: earlier, + endTime: now, + recordingMethod: RecordingMethod.manual, + ) != + null; } setState(() { @@ -524,15 +649,20 @@ class HealthAppState extends State { bool stepsPermission = await health.hasPermissions([HealthDataType.STEPS]) ?? false; if (!stepsPermission) { - stepsPermission = - await health.requestAuthorization([HealthDataType.STEPS]); + stepsPermission = await health.requestAuthorization([ + HealthDataType.STEPS, + ]); } if (stepsPermission) { try { - steps = await health.getTotalStepsInInterval(midnight, now, - includeManualEntry: - !recordingMethodsToFilter.contains(RecordingMethod.manual)); + steps = await health.getTotalStepsInInterval( + midnight, + now, + includeManualEntry: !recordingMethodsToFilter.contains( + RecordingMethod.manual, + ), + ); } catch (error) { debugPrint("Exception in getTotalStepsInInterval: $error"); } @@ -573,17 +703,18 @@ class HealthAppState extends State { final startDate = DateTime.now().subtract(const Duration(days: 7)); final endDate = DateTime.now(); - List healthDataResponse = - await health.getHealthIntervalDataFromTypes( - startDate: startDate, - endDate: endDate, - types: [HealthDataType.BLOOD_OXYGEN, HealthDataType.STEPS], - interval: 86400, // 86400 seconds = 1 day - // recordingMethodsToFilter: recordingMethodsToFilter, - ); + List healthDataResponse = await health + .getHealthIntervalDataFromTypes( + startDate: startDate, + endDate: endDate, + types: [HealthDataType.BLOOD_OXYGEN, HealthDataType.STEPS], + interval: 86400, // 86400 seconds = 1 day + // recordingMethodsToFilter: recordingMethodsToFilter, + ); debugPrint( - 'Total number of interval data points: ${healthDataResponse.length}. ' - '${healthDataResponse.length > 100 ? 'Only showing the first 100.' : ''}'); + 'Total number of interval data points: ${healthDataResponse.length}. ' + '${healthDataResponse.length > 100 ? 'Only showing the first 100.' : ''}', + ); debugPrint("Interval data points: "); for (var data in healthDataResponse) { @@ -592,9 +723,11 @@ class HealthAppState extends State { healthDataResponse.sort((a, b) => b.dateTo.compareTo(a.dateTo)); _healthDataList.clear(); - _healthDataList.addAll((healthDataResponse.length < 100) - ? healthDataResponse - : healthDataResponse.sublist(0, 100)); + _healthDataList.addAll( + (healthDataResponse.length < 100) + ? healthDataResponse + : healthDataResponse.sublist(0, 100), + ); for (var data in _healthDataList) { debugPrint(toJsonString(data)); @@ -616,9 +749,8 @@ class HealthAppState extends State { shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), - builder: (BuildContext context) => _detailedBottomSheet( - healthPoint: healthPoint, - ), + builder: (BuildContext context) => + _detailedBottomSheet(healthPoint: healthPoint), ); } @@ -628,9 +760,7 @@ class HealthAppState extends State { Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - appBar: AppBar( - title: const Text('Health Example'), - ), + appBar: AppBar(title: const Text('Health Example')), body: Column( children: [ Wrap( @@ -638,81 +768,133 @@ class HealthAppState extends State { children: [ if (Platform.isAndroid) TextButton( - onPressed: getHealthConnectSdkStatus, - style: const ButtonStyle( - backgroundColor: WidgetStatePropertyAll(Colors.blue)), - child: const Text("Check Health Connect Status", - style: TextStyle(color: Colors.white))), + 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))), + 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( + Wrap( + spacing: 10, + children: [ + TextButton( onPressed: authorize, style: const ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Colors.blue)), - child: const Text("Authenticate", - style: TextStyle(color: Colors.white))), - TextButton( + backgroundColor: WidgetStatePropertyAll(Colors.blue), + ), + child: const Text( + "Authenticate", + style: TextStyle(color: Colors.white), + ), + ), + TextButton( onPressed: fetchData, style: const ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Colors.blue)), - child: const Text("Fetch Data", - style: TextStyle(color: Colors.white))), - TextButton( + backgroundColor: 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( + backgroundColor: WidgetStatePropertyAll(Colors.blue), + ), + child: const Text( + "Add Data", + style: TextStyle(color: Colors.white), + ), + ), + TextButton( + onPressed: addSingleHealthData, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.blue), + ), + child: const Text( + "Add Steps Data", + style: TextStyle(color: Colors.white), + ), + ), + TextButton( + onPressed: addSingleWorkoutData, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.blue), + ), + child: const Text( + "Add Running 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( + 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( + 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))), - TextButton( + backgroundColor: WidgetStatePropertyAll(Colors.blue), + ), + child: const Text( + "Revoke Access", + style: TextStyle(color: Colors.white), + ), + ), + TextButton( onPressed: getIntervalBasedData, style: const ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Colors.blue)), - child: const Text('Get Interval Data (7 days)', - style: TextStyle(color: Colors.white))), - ]), + backgroundColor: WidgetStatePropertyAll(Colors.blue), + ), + child: const Text( + 'Get Interval Data (7 days)', + 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)) + Expanded(child: Center(child: _content)), ], ), ), @@ -720,94 +902,91 @@ class HealthAppState extends State { } Widget get _dataFiltration => Column( + children: [ + Wrap( children: [ - Wrap( - children: [ - for (final method in Platform.isAndroid + for (final method + in Platform.isAndroid ? [ RecordingMethod.manual, RecordingMethod.automatic, RecordingMethod.active, RecordingMethod.unknown, ] - : [ - RecordingMethod.automatic, - RecordingMethod.manual, - ]) - SizedBox( - width: 150, - child: CheckboxListTile( - title: Text( - '${method.name[0].toUpperCase()}${method.name.substring(1)} entries'), - value: !recordingMethodsToFilter.contains(method), - onChanged: (value) { - setState(() { - if (value!) { - recordingMethodsToFilter.remove(method); - } else { - recordingMethodsToFilter.add(method); - } - fetchData(); - }); - }, - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - dense: true, - ), + : [RecordingMethod.automatic, RecordingMethod.manual]) + SizedBox( + width: 150, + child: CheckboxListTile( + title: Text( + '${method.name[0].toUpperCase()}${method.name.substring(1)} entries', ), - // Add other entries here if needed - ], - ), - const Divider(thickness: 3), + value: !recordingMethodsToFilter.contains(method), + onChanged: (value) { + setState(() { + if (value!) { + recordingMethodsToFilter.remove(method); + } else { + recordingMethodsToFilter.add(method); + } + fetchData(); + }); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + dense: true, + ), + ), + // Add other entries here if needed ], - ); + ), + const Divider(thickness: 3), + ], + ); Widget get _stepsFiltration => Column( + children: [ + Wrap( children: [ - Wrap( - children: [ - for (final method in [ - RecordingMethod.manual, - ]) - SizedBox( - width: 150, - child: CheckboxListTile( - title: Text( - '${method.name[0].toUpperCase()}${method.name.substring(1)} entries'), - value: !recordingMethodsToFilter.contains(method), - onChanged: (value) { - setState(() { - if (value!) { - recordingMethodsToFilter.remove(method); - } else { - recordingMethodsToFilter.add(method); - } - fetchStepData(); - }); - }, - controlAffinity: ListTileControlAffinity.leading, - contentPadding: EdgeInsets.zero, - dense: true, - ), + for (final method in [RecordingMethod.manual]) + SizedBox( + width: 150, + child: CheckboxListTile( + title: Text( + '${method.name[0].toUpperCase()}${method.name.substring(1)} entries', ), - // Add other entries here if needed - ], - ), - const Divider(thickness: 3), + value: !recordingMethodsToFilter.contains(method), + onChanged: (value) { + setState(() { + if (value!) { + recordingMethodsToFilter.remove(method); + } else { + recordingMethodsToFilter.add(method); + } + fetchStepData(); + }); + }, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + dense: true, + ), + ), + // Add other entries here if needed ], - ); + ), + const Divider(thickness: 3), + ], + ); Widget get _permissionsRevoking => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - child: const CircularProgressIndicator( - strokeWidth: 10, - )), - const Text('Revoking permissions...') - ], - ); + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + child: const CircularProgressIndicator(strokeWidth: 10), + ), + const Text('Revoking permissions...'), + ], + ); Widget get _permissionsRevoked => const Text('Permissions revoked.'); @@ -815,102 +994,97 @@ class HealthAppState extends State { const Text('Failed to revoke permissions'); Widget get _contentFetchingData => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - child: const CircularProgressIndicator( - strokeWidth: 10, - )), - const Text('Fetching data...') - ], - ); + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + child: const CircularProgressIndicator(strokeWidth: 10), + ), + const Text('Fetching data...'), + ], + ); - Widget get _contentDataReady => Builder(builder: (context) { - return ListView.builder( - itemCount: _healthDataList.length, - itemBuilder: (_, index) { - // filter out manual entires if not wanted - if (recordingMethodsToFilter - .contains(_healthDataList[index].recordingMethod)) { - return Container(); - } - - HealthDataPoint p = _healthDataList[index]; - if (p.value is AudiogramHealthValue) { - return ListTile( - title: Text("${p.typeString}: ${p.value}"), - trailing: Text(p.unitString), - subtitle: - Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), - onTap: () { - fetchDataByUUID( - context, - uuid: p.uuid, - type: p.type, - ); - }, - ); - } - if (p.value is WorkoutHealthValue) { - 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), - subtitle: - Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), - onTap: () { - fetchDataByUUID( - context, - uuid: p.uuid, - type: p.type, - ); - }, - ); - } - if (p.value is NutritionHealthValue) { - return ListTile( - title: Text( - "${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}"), - trailing: Text( - '${(p.value as NutritionHealthValue).calories} kcal'), - subtitle: - Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), - onTap: () { - fetchDataByUUID( - context, - uuid: p.uuid, - type: p.type, - ); - }, - ); - } - return ListTile( - title: Text("${p.typeString}: ${p.value}"), - trailing: Text(p.unitString), - subtitle: - Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), - onTap: () { - fetchDataByUUID( - context, - uuid: p.uuid, - type: p.type, - ); - }, - ); - }); - }); + Widget get _contentDataReady => Builder( + builder: (context) { + return ListView.builder( + itemCount: _healthDataList.length, + itemBuilder: (_, index) { + // filter out manual entires if not wanted + if (recordingMethodsToFilter.contains( + _healthDataList[index].recordingMethod, + )) { + return Container(); + } + + HealthDataPoint p = _healthDataList[index]; + if (p.value is AudiogramHealthValue) { + return ListTile( + title: Text("${p.typeString}: ${p.value}"), + trailing: Text(p.unitString), + subtitle: Text( + '${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}', + ), + onTap: () { + fetchDataByUUID(context, uuid: p.uuid, type: p.type); + }, + ); + } + if (p.value is WorkoutHealthValue) { + 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, + ), + subtitle: Text( + '${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}', + ), + onTap: () { + fetchDataByUUID(context, uuid: p.uuid, type: p.type); + }, + ); + } + if (p.value is NutritionHealthValue) { + return ListTile( + title: Text( + "${p.typeString} ${(p.value as NutritionHealthValue).mealType}: ${(p.value as NutritionHealthValue).name}", + ), + trailing: Text( + '${(p.value as NutritionHealthValue).calories} kcal', + ), + subtitle: Text( + '${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}', + ), + onTap: () { + fetchDataByUUID(context, uuid: p.uuid, type: p.type); + }, + ); + } + return ListTile( + title: Text("${p.typeString}: ${p.value}"), + trailing: Text(p.unitString), + subtitle: Text('${p.dateFrom} - ${p.dateTo}\n${p.recordingMethod}'), + onTap: () { + fetchDataByUUID(context, uuid: p.uuid, type: p.type); + }, + ); + }, + ); + }, + ); final Widget _contentNoData = const Text('No Data to show'); - final Widget _contentNotFetched = - const Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - 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."), - ]); + final Widget _contentNotFetched = const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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."), + ], + ); final Widget _authorized = const Text('Authorization granted!'); @@ -919,13 +1093,15 @@ class HealthAppState extends State { children: [ Text('Authorization not given.'), Text( - 'For Google Health Connect please check if you have added the right permissions and services to the manifest file.'), + 'For Google Health Connect please check if you have added the right permissions and services to the manifest file.', + ), Text('For Apple Health check your permissions in Apple Health.'), ], ); Widget _contentHealthConnectStatus = const Text( - 'No status, click getHealthConnectSdkStatus to get the status.'); + 'No status, click getHealthConnectSdkStatus to get the status.', + ); final Widget _dataAdded = const Text('Data points inserted successfully.'); @@ -933,28 +1109,29 @@ class HealthAppState extends State { Widget get _stepsFetched => Text('Total number of steps: $_nofSteps.'); - final Widget _dataNotAdded = - const Text('Failed to add data.\nDo you have permissions to add data?'); + final Widget _dataNotAdded = const Text( + 'Failed to add data.\nDo you have permissions to add data?', + ); final Widget _dataNotDeleted = const Text('Failed to delete data'); Widget get _content => switch (_state) { - AppState.DATA_READY => _contentDataReady, - AppState.DATA_NOT_FETCHED => _contentNotFetched, - AppState.FETCHING_DATA => _contentFetchingData, - AppState.NO_DATA => _contentNoData, - AppState.AUTHORIZED => _authorized, - AppState.AUTH_NOT_GRANTED => _authorizationNotGranted, - AppState.DATA_ADDED => _dataAdded, - AppState.DATA_DELETED => _dataDeleted, - AppState.DATA_NOT_ADDED => _dataNotAdded, - AppState.DATA_NOT_DELETED => _dataNotDeleted, - AppState.STEPS_READY => _stepsFetched, - AppState.HEALTH_CONNECT_STATUS => _contentHealthConnectStatus, - AppState.PERMISSIONS_REVOKING => _permissionsRevoking, - AppState.PERMISSIONS_REVOKED => _permissionsRevoked, - AppState.PERMISSIONS_NOT_REVOKED => _permissionsNotRevoked, - }; + AppState.DATA_READY => _contentDataReady, + AppState.DATA_NOT_FETCHED => _contentNotFetched, + AppState.FETCHING_DATA => _contentFetchingData, + AppState.NO_DATA => _contentNoData, + AppState.AUTHORIZED => _authorized, + AppState.AUTH_NOT_GRANTED => _authorizationNotGranted, + AppState.DATA_ADDED => _dataAdded, + AppState.DATA_DELETED => _dataDeleted, + AppState.DATA_NOT_ADDED => _dataNotAdded, + AppState.DATA_NOT_DELETED => _dataNotDeleted, + AppState.STEPS_READY => _stepsFetched, + AppState.HEALTH_CONNECT_STATUS => _contentHealthConnectStatus, + AppState.PERMISSIONS_REVOKING => _permissionsRevoking, + AppState.PERMISSIONS_REVOKED => _permissionsRevoked, + AppState.PERMISSIONS_NOT_REVOKED => _permissionsNotRevoked, + }; Widget _detailedBottomSheet({HealthDataPoint? healthPoint}) { return DraggableScrollableSheet( @@ -979,15 +1156,17 @@ class HealthAppState extends State { controller: scrollController, itemCount: healthPoint.toJson().entries.length, itemBuilder: (context, index) { - String key = - healthPoint.toJson().keys.elementAt(index); + String key = healthPoint.toJson().keys.elementAt( + index, + ); var value = healthPoint.toJson()[key]; return ListTile( title: Text( key.replaceAll('_', ' ').toUpperCase(), - style: - const TextStyle(fontWeight: FontWeight.bold), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), ), subtitle: Text(value.toString()), ); diff --git a/packages/health/ios/Classes/HealthDataWriter.swift b/packages/health/ios/Classes/HealthDataWriter.swift index df19a505e..18805f7fc 100644 --- a/packages/health/ios/Classes/HealthDataWriter.swift +++ b/packages/health/ios/Classes/HealthDataWriter.swift @@ -73,7 +73,17 @@ class HealthDataWriter { print("Error Saving \(type) Sample: \(err.localizedDescription)") } DispatchQueue.main.async { - result(success) + if success { + // Return the UUID of the saved object + if let savedSample = sample as? HKObject { + print("Saved: \(savedSample.uuid.uuidString)") + result(savedSample.uuid.uuidString) // Return UUID as String + } else { + result("") + } + } + + result("") } }) } @@ -433,7 +443,18 @@ class HealthDataWriter { print("Error Saving Workout. Sample: \(err.localizedDescription)") } DispatchQueue.main.async { - result(success) + + if success { + // Return the UUID of the saved object + if let savedSample = workout as? HKWorkout { + print("Saved: \(savedSample.uuid.uuidString)") + result(savedSample.uuid.uuidString) // Return UUID as String + } else { + result("") + } + } + + result("") } }) } diff --git a/packages/health/lib/src/health_plugin.dart b/packages/health/lib/src/health_plugin.dart index 622e3a937..57d8f4a10 100644 --- a/packages/health/lib/src/health_plugin.dart +++ b/packages/health/lib/src/health_plugin.dart @@ -489,7 +489,7 @@ class Health { /// Write health data. /// - /// Returns true if successful, false otherwise. + /// Returns created HealthDataPoint if successful, null otherwise. /// /// Parameters: /// * [value] - the health data's value in double @@ -506,7 +506,7 @@ class Health { /// /// Values for Sleep and Headache are ignored and will be automatically assigned /// the default value. - Future writeHealthData({ + Future writeHealthData({ required double value, HealthDataUnit? unit, required HealthDataType type, @@ -579,8 +579,10 @@ class Health { 'clientRecordId': clientRecordId, 'clientRecordVersion': clientRecordVersion, }; - bool? success = await _channel.invokeMethod('writeData', args); - return success ?? false; + + String uuid = '${await _channel.invokeMethod('writeData', args)}'; + + return uuid; } /// Deletes all records of the given [type] for a given period of time. @@ -744,13 +746,15 @@ class Health { bool? success; if (Platform.isIOS) { - success = await writeHealthData( + final healthPoint = await writeHealthData( value: saturation, type: HealthDataType.BLOOD_OXYGEN, startTime: startTime, endTime: endTime, recordingMethod: recordingMethod, ); + + success = healthPoint != null; } else if (Platform.isAndroid) { Map args = { 'value': saturation, @@ -759,7 +763,9 @@ class Health { 'dataTypeKey': HealthDataType.BLOOD_OXYGEN.name, 'recordingMethod': recordingMethod.toInt(), }; - success = await _channel.invokeMethod('writeBloodOxygen', args); + // Check if UUID is not empty + success = + '${await _channel.invokeMethod('writeBloodOxygen', args)}'.isNotEmpty; } return success ?? false; } @@ -1483,7 +1489,7 @@ class Health { /// Write workout data to Apple Health or Google Health Connect. /// - /// Returns true if the workout data was successfully added. + /// Returns created HealthDataPoint if the workout data was successfully added, null otherwise. /// /// Parameters: /// - [activityType] The type of activity performed. @@ -1498,7 +1504,7 @@ class Health { /// - [title] The title of the workout. /// *ONLY FOR HEALTH CONNECT* Default value is the [activityType], e.g. "STRENGTH_TRAINING". /// - [recordingMethod] The recording method of the data point, automatic by default (on iOS this can only be automatic or manual). - Future writeWorkoutData({ + Future writeWorkoutData({ required HealthWorkoutActivityType activityType, required DateTime start, required DateTime end, @@ -1541,7 +1547,10 @@ class Health { 'title': title, 'recordingMethod': recordingMethod.toInt(), }; - return await _channel.invokeMethod('writeWorkoutData', args) == true; + + String uuid = '${await _channel.invokeMethod('writeWorkoutData', args)}'; + + return uuid; } /// Check if the given [HealthWorkoutActivityType] is supported on the iOS platform diff --git a/packages/health/pubspec.yaml b/packages/health/pubspec.yaml index a55907070..b4f8185d7 100644 --- a/packages/health/pubspec.yaml +++ b/packages/health/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter intl: '>=0.18.0 <0.21.0' - device_info_plus: '12.1.0' + device_info_plus: '^12.1.0' json_annotation: ^4.9.0 carp_serializable: ^2.0.1 # polymorphic json serialization