Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
323b454
Update health.json.dart
Nov 5, 2024
4484e45
Update HealthPlugin.kt
Nov 28, 2024
2e5f661
Merge pull request #1 from cph-cachet/master
agilst Feb 11, 2025
f6a73a9
Update example app to align with latest Flutter version
Mar 17, 2025
ff49189
Merge branch 'master' of https://github.com/agilst/flutter-plugins
Mar 18, 2025
0cb04ab
Merge branch 'cph-cachet:master' into master
agilst May 2, 2025
702b213
Add getHealthDataByUUID for iOS
May 2, 2025
9f61828
Add getDataByUUID for Android
May 2, 2025
d91a94b
Fix example Android build sdk version
May 2, 2025
827fbde
add delete by client id and fetch unit support
connor-callaghan Jun 25, 2025
42e0b73
add better null safety
connor-callaghan Jun 26, 2025
141c877
remove unused imports
connor-callaghan Jun 26, 2025
93851f9
Merge branch 'master' of https://github.com/agilst/flutter-plugins
agilst Jul 17, 2025
b9f9049
Merge branch 'master' into 1139-single-record-ios
agilst Jul 17, 2025
9a8972f
Sync with latest master changes
agilst Jul 17, 2025
a0aed49
Merge branch 'master' into 1150-single-record-android
agilst Jul 17, 2025
88a0505
Revert android build.gradle to original and bump targetSdk
agilst Jul 18, 2025
1cea6b0
Sync getDataByUUID with latest master changes
agilst Jul 18, 2025
75f4961
Fix getDataByUUID for iOS
agilst Jul 18, 2025
aa9eec8
Revert write data to original
agilst Jul 18, 2025
9369bb3
allowing the units to be used when parsing the HealthDataPoint
StuartCameron98 Jul 29, 2025
f47daa8
making null safe
StuartCameron98 Jul 29, 2025
5ac55d0
Merge pull request #1 from StuartCameron98/stuart/unit-fixing
connor-callaghan Jul 30, 2025
be9f0a2
Clean up
iarata Aug 30, 2025
4f05ae9
Update to SDK 3.8.0 for health.g.dart
iarata Aug 30, 2025
316af9d
Merge branch 'health13/prs' into 1139-single-record-ios
iarata Aug 30, 2025
7a9a694
Clean up
iarata Aug 31, 2025
57d9ec1
Merge branch 'health13/prs' into 1150-single-record-android
iarata Aug 31, 2025
594a835
Merge pull request #1193 from agilst/1139-single-record-ios
iarata Aug 31, 2025
bedfceb
Merge branch 'health13/prs' into 1150-single-record-android
iarata Aug 31, 2025
95a2478
Merge pull request #1194 from agilst/1150-single-record-android
iarata Aug 31, 2025
0eb78dd
Fix ECG processing in the get by UUID method for iOS
iarata Aug 31, 2025
751aa7e
Bump to 13.2.0
iarata Aug 31, 2025
aaebae2
Merge branch 'health13/prs' into delete-by-id-and-unit-support
iarata Aug 31, 2025
aa247ab
only get the stuff we have permissions for
StuartCameron98 Sep 9, 2025
4d5e535
Merge pull request #2 from connor-callaghan/stuart/reading-permisions
connor-callaghan Sep 10, 2025
e14c8b4
build_runner
iarata Sep 16, 2025
dbf5c68
Fix int to long casting issue
iarata Sep 16, 2025
8193218
Merge pull request #1223 from connor-callaghan/delete-by-id-and-unit-…
iarata Sep 16, 2025
22a9e1d
Bump to 13.2.0, merge PRs and dependencies update
iarata Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/health/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## 13.2.0

* Add get health data by UUID (see `getHealthDataByUUID()`) - PR [#1193](https://github.com/carp-dk/flutter-plugins/pull/1193), [#1194](https://github.com/carp-dk/flutter-plugins/pull/1194)
* Add delete by UUID (`deleteByUUID()`)
* Add support for unit conversion in `WeightRecord`, `HeightRecord`, `BodyTemperatureRecord`, and `BloodGlucoseRecord` - PR [#1212](https://github.com/carp-dk/flutter-plugins/pull/1223)
* Update `compileSDK` to 36 - Fix [#1261](https://github.com/carp-dk/flutter-plugins/issues/1261)
* Update Gradle to 8.9.1
* Update `org.jetbrains.kotlin.android` to 2.1.0
* Update `androidx.health.connect:connect-client` to 1.1.0-rc03
* Update `device_info_plus` to 12.1.0 - Fix [#1264](https://github.com/carp-dk/flutter-plugins/issues/1264)

## 13.1.4

* Fix adding mindfulness resulted in crash in iOS
Expand Down
53 changes: 53 additions & 0 deletions packages/health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,22 @@ flutter: Health Plugin Error:
flutter: PlatformException(FlutterHealth, Results are null, Optional(Error Domain=com.apple.healthkit Code=6 "Protected health data is inaccessible" UserInfo={NSLocalizedDescription=Protected health data is inaccessible}))
```

### Fetch single health data by UUID

In order to retrieve a single record, it is required to provide `String uuid` and `HealthDataType type`.

Please see example below:
```dart
HealthDataPoint? healthPoint = await health.getHealthDataByUUID(
uuid: 'random-uuid-string',
type: HealthDataType.STEPS,
);
```
```
I/FLUTTER_HEALTH( 9161): Success: {uuid=random-uuid-string, value=12, date_from=1742259061009, date_to=1742259092888, source_id=, source_name=com.google.android.apps.fitness, recording_method=0}
```
> Assuming that the `uuid` and `type` are coming from your database.

### Filtering by recording method

Google Health Connect and Apple HealthKit both provide ways to distinguish samples collected "automatically" and manually entered data by the user.
Expand Down Expand Up @@ -322,6 +338,43 @@ Furthermore, the plugin now exposes three new functions to help you check and re
2. `isHealthDataInBackgroundAuthorized()`: Checks the current status of the Health Data in Background permission
3. `requestHealthDataInBackgroundAuthorization()`: Requests the Health Data in Background permission.

### Fetch single health data by UUID

In order to retrieve a single record, it is required to provide `String uuid` and `HealthDataType type`.

Please see example below:
```dart
HealthDataPoint? healthPoint = await health.getHealthDataByUUID(
uuid: 'E9F2EEAD-8FC5-4CE5-9FF5-7C4E535FB8B8',
type: HealthDataType.WORKOUT,
);
```
```
data by UUID: HealthDataPoint -
uuid: E9F2EEAD-8FC5-4CE5-9FF5-7C4E535FB8B8,
value: WorkoutHealthValue - workoutActivityType: RUNNING,
totalEnergyBurned: null,
totalEnergyBurnedUnit: KILOCALORIE,
totalDistance: 2400,
totalDistanceUnit: METER
totalSteps: null,
totalStepsUnit: null,
unit: NO_UNIT,
dateFrom: 2025-05-02 07:31:00.000,
dateTo: 2025-05-02 08:25:00.000,
dataType: WORKOUT,
platform: HealthPlatformType.appleHealth,
deviceId: unknown,
sourceId: com.apple.Health,
sourceName: Health
recordingMethod: RecordingMethod.manual
workoutSummary: WorkoutSummary - workoutType: runningtotalDistance: 2400, totalEnergyBurned: 0, totalSteps: 0
metadata: null
deviceModel: null
```
> Assuming that the `uuid` and `type` are coming from your database.


## Data Types

The plugin supports the following [`HealthDataType`](https://pub.dev/documentation/health/latest/health/HealthDataType.html).
Expand Down
15 changes: 7 additions & 8 deletions packages/health/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:8.1.4'
classpath 'com.android.tools.build:gradle:8.13.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand All @@ -25,7 +25,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdk 34
compileSdk 36

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
Expand All @@ -41,7 +41,7 @@ android {
}
defaultConfig {
minSdkVersion 26
targetSdkVersion 34
targetSdkVersion 36
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
Expand All @@ -51,12 +51,11 @@ android {
}

dependencies {
def composeBom = platform('androidx.compose:compose-bom:2025.02.00')
def composeBom = platform('androidx.compose:compose-bom:2025.09.00')
implementation(composeBom)
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10"

implementation("androidx.health.connect:connect-client:1.1.0-alpha11")
def fragment_version = "1.8.6"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation("androidx.health.connect:connect-client:1.1.0-rc03")
implementation "androidx.fragment:fragment-ktx:1.8.9"

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#Tue Sep 16 09:25:00 CEST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,34 @@ class HealthDataConverter {
* @return List<Map<String, Any?>> List of converted records (some records may split into multiple entries)
* @throws IllegalArgumentException If the record type is not supported
*/
fun convertRecord(record: Any, dataType: String): List<Map<String, Any?>> {
fun convertRecord(record: Any, dataType: String, dataUnit: String? = null): List<Map<String, Any?>> {
val metadata = (record as Record).metadata

return when (record) {
// Single-value instant records
is WeightRecord -> listOf(createInstantRecord(metadata, record.time, record.weight.inKilograms))
is HeightRecord -> listOf(createInstantRecord(metadata, record.time, record.height.inMeters))
is WeightRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) {
"POUND" -> record.weight.inPounds
else -> record.weight.inKilograms
}))
is HeightRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) {
"CENTIMETER" -> (record.height.inMeters * 100)
"INCH" -> record.height.inInches
else -> record.height.inMeters
}))
is BodyFatRecord -> listOf(createInstantRecord(metadata, record.time, record.percentage.value))
is LeanBodyMassRecord -> listOf(createInstantRecord(metadata, record.time, record.mass.inKilograms))
is HeartRateVariabilityRmssdRecord -> listOf(createInstantRecord(metadata, record.time, record.heartRateVariabilityMillis))
is BodyTemperatureRecord -> listOf(createInstantRecord(metadata, record.time, record.temperature.inCelsius))
is BodyTemperatureRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) {
"DEGREE_FAHRENHEIT" -> record.temperature.inFahrenheit
"KELVIN" -> record.temperature.inCelsius + 273.15
else -> record.temperature.inCelsius
}))
is BodyWaterMassRecord -> listOf(createInstantRecord(metadata, record.time, record.mass.inKilograms))
is OxygenSaturationRecord -> listOf(createInstantRecord(metadata, record.time, record.percentage.value))
is BloodGlucoseRecord -> listOf(createInstantRecord(metadata, record.time, record.level.inMilligramsPerDeciliter))
is BloodGlucoseRecord -> listOf(createInstantRecord(metadata, record.time, when (dataUnit) {
"MILLIMOLES_PER_LITER" -> record.level.inMillimolesPerLiter
else -> record.level.inMilligramsPerDeciliter
}))
is BasalMetabolicRateRecord -> listOf(createInstantRecord(metadata, record.time, record.basalMetabolicRate.inKilocaloriesPerDay))
is RestingHeartRateRecord -> listOf(createInstantRecord(metadata, record.time, record.beatsPerMinute))
is RespiratoryRateRecord -> listOf(createInstantRecord(metadata, record.time, record.rate))
Expand Down Expand Up @@ -236,7 +250,7 @@ class HealthDataConverter {
)
)
}

companion object {
private const val BLOOD_PRESSURE_DIASTOLIC = "BLOOD_PRESSURE_DIASTOLIC"
private const val MEAL_UNKNOWN = "UNKNOWN"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cachet.plugins.health
import android.util.Log
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.HealthConnectFeatures
import androidx.health.connect.client.feature.ExperimentalFeatureAvailabilityApi
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_HISTORY
import androidx.health.connect.client.permission.HealthPermission.Companion.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND
Expand Down Expand Up @@ -103,7 +102,6 @@ class HealthDataOperations(
* @param call Method call from Flutter (unused)
* @param result Flutter result callback returning boolean availability status
*/
@OptIn(ExperimentalFeatureAvailabilityApi::class)
fun isHealthDataHistoryAvailable(call: MethodCall, result: Result) {
scope.launch {
result.success(
Expand Down Expand Up @@ -139,7 +137,6 @@ class HealthDataOperations(
* @param call Method call from Flutter (unused)
* @param result Flutter result callback returning boolean availability status
*/
@OptIn(ExperimentalFeatureAvailabilityApi::class)
fun isHealthDataInBackgroundAvailable(call: MethodCall, result: Result) {
scope.launch {
result.success(
Expand Down Expand Up @@ -247,6 +244,45 @@ class HealthDataOperations(
}
}

/**
* Deletes a specific health record by its client record ID and data type. Allows precise
* deletion of individual health records using client-side IDs.
*
* @param call Method call containing 'dataTypeKey', 'recordId', and 'clientRecordId'
* @param result Flutter result callback returning boolean success status
*/
fun deleteByClientRecordId(call: MethodCall, result: Result) {
val arguments = call.arguments as? HashMap<*, *>
val dataTypeKey = (arguments?.get("dataTypeKey") as? String)!!
val recordId = listOfNotNull(arguments["recordId"] as? String)
val clientRecordId = listOfNotNull(arguments["clientRecordId"] as? String)
if (!HealthConstants.mapToType.containsKey(dataTypeKey)) {
Log.w("FLUTTER_HEALTH::ERROR", "Datatype $dataTypeKey not found in HC")
result.success(false)
return
}
val classType = HealthConstants.mapToType[dataTypeKey]!!

scope.launch {
try {
healthConnectClient.deleteRecords(
classType,
recordId,
clientRecordId
)
result.success(true)
} catch (e: Exception) {
Log.e(
"FLUTTER_HEALTH::ERROR",
"Error deleting record with ClientRecordId: $clientRecordId"
)
Log.e("FLUTTER_HEALTH::ERROR", e.message ?: "unknown error")
Log.e("FLUTTER_HEALTH::ERROR", e.stackTraceToString())
result.success(false)
}
}
}

/**
* Internal helper method to prepare Health Connect permission strings. Converts data type names
* and access levels into proper permission format.
Expand Down
Loading