Kotlin Multiplatform Mobile wrapper for HealthKit on iOS, and Google Fit or Health Connect on Android.
Google Fitness API is being deprecated and HealthKMP will try to use Health Connect if the app is installed.
HealthKMP supports:
- checking if any of health service is available on the device.
- handling permissions to access health data.
- reading health data.
- writing health data.
- aggregating health data.
Note that for Android, the target phone needs to have Google Fit or Health Connect installed.
- Sleep
- Steps
- Weight
To access health data users need to grant permissions
First add the dependency to your project:
settings.gradle.kts:
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
build.gradle:
sourceSets {
val commonMain by getting {
dependencies {
implementation("com.viktormykhailiv:health-kmp:0.0.6")
}
}
}
Step 1: Append the Info.plist with the following 2 entries
<key>NSHealthShareUsageDescription</key>
<string>We will sync your data with the Apple Health app to give you better insights</string>
<key>NSHealthUpdateUsageDescription</key>
<string>We will sync your data with the Apple Health app to give you better insights</string>
Step 2: Enable "HealthKit" by adding a capability inside the "Signing & Capabilities" tab of the Runner target's settings.
Using Health Connect on Android requires special permissions in the AndroidManifest.xml
file.
The permissions can be found here: https://developer.android.com/guide/health-and-fitness/health-connect/data-and-data-types/data-types
Example shown here (can also be found in the sample app):
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
Follow the guide at https://developers.google.com/fit/android/get-api-key
Below is an example of following the guide:
Change directory to your key-store directory (MacOS):
cd ~/.android/
Get your keystore SHA1 fingerprint:
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
Example output:
Alias name: androiddebugkey
Creation date: Aug 8, 2023
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: C=US, O=Android, CN=Android Debug
Issuer: C=US, O=Android, CN=Android Debug
Serial number: 4aa9b300
Valid from: Tue Aug 01 10:07:15 CEST 2023 until: Thu Jul 24 10:07:15 CEST 2053
Certificate fingerprints:
MD5: E4:D7:F1:B4:ED:2E:42:D1:58:98:F4:B2:7B:01:9D:A4
SHA1: E4:D7:F1:B4:ED:2E:42:D1:58:98:F4:B2:7B:01:9D:A4:E4:D7:F1:B4
Signature algorithm name: SHA256withRSA
Subject Public Key Algorithm: 2048-bit RSA key
Version: 1
Follow the instructions at https://developers.google.com/fit/android/get-api-key for setting up an OAuth2 Client ID for a Google project, and adding the SHA1 fingerprint to that OAuth2 credential.
The client id will look something like YOUR_CLIENT_ID.apps.googleusercontent.com
.
See the sample app for detailed examples of how to use the HealthKMP API.
The Health plugin is used via the HealthManagerFactory
class using the different methods for handling permissions and getting and adding data to Apple Health / Health Connect / Google Fit.
Check if any Health service is available on the device: HealthKit on iOS, and Google Fit or Health Connect on Android
val health = HealthManagerFactory().createManager()
health.isAvailable()
.onSuccess { isAvailable ->
if (!isAvailable) {
println("No Health service is available on the device")
}
}
.onFailure { error ->
println("Failed to check if Health service is available $error")
}
Requesting access to data types before reading them
health.requestAuthorization(
readTypes = listOf(
HealthDataType.Sleep,
HealthDataType.Steps,
HealthDataType.Weight,
),
writeTypes = listOf(
HealthDataType.Sleep,
HealthDataType.Steps,
HealthDataType.Weight,
),
)
.onSuccess { isAuthorized ->
if (!isAuthorized) {
println("Not authorized")
}
}
.onFailure { error ->
println("Failed to authorize $error")
}
Read detailed sleep data for last 24 hours
health.readSleep(
startTime = Clock.System.now().minus(24.hours),
endTime = Clock.System.now(),
).onSuccess { sleepRecords ->
sleepRecords.forEach { sleep ->
println("Sleep duration ${sleep.duration}")
// Calculate duration of each sleep stage
sleep.stages.groupBy { it.type }
.forEach { (type, stages) ->
val stageDuration = stages.sumOf { it.duration.inWholeMinutes }.minutes
println("Sleep stage $type $stageDuration")
}
}
if (sleepRecords.isEmpty()) {
println("No sleep data")
}
}.onFailure { error ->
println("Failed to read sleep $error")
}
Read aggregated sleep data for last month
health.aggregateSleep(
startTime = Clock.System.now().minus(30.days),
endTime = Clock.System.now(),
).onSuccess { sleep ->
println("Sleep total duration ${sleep.totalDuration}")
}
Read detailed steps data for last day
health.readSteps(
startTime = Clock.System.now().minus(1.days),
endTime = Clock.System.now(),
).onSuccess { steps ->
steps.forEachIndexed { index, record ->
println("[$index] ${record.count} steps for ${record.duration}")
}
if (steps.isEmpty()) {
println("No steps data")
}
}.onFailure { error ->
println("Failed to read steps $error")
}
Read aggregated steps data for last day
health.aggregateSteps(
startTime = Clock.System.now().minus(1.days),
endTime = Clock.System.now(),
).onSuccess { steps ->
println("Steps total ${steps.count}")
}
Read detailed weight data for last year
health.readWeight(
startTime = Clock.System.now().minus(365.days),
endTime = Clock.System.now(),
).onSuccess { records ->
records.forEachIndexed { index, record ->
println("[$index] ${record.weight} at ${record.time}")
}
if (records.isEmpty()) {
println("No weight data")
}
}.onFailure { error ->
println("Failed to read weight $error")
}
Read aggregated weight data for last year
health.aggregateWeight(
startTime = Clock.System.now().minus(365.hours),
endTime = Clock.System.now(),
).onSuccess { weight ->
println("Weight avg ${weight.avg} kg, min ${weight.min}, max ${weight.max}")
}
Write sleep data for 1 hours
val startTime = Clock.System.now().minus(12.hours)
val endTime = Clock.System.now().minus(11.hours)
val types = listOf(
SleepStageType.Awake,
SleepStageType.OutOfBed,
SleepStageType.Sleeping,
SleepStageType.Light,
SleepStageType.Deep,
SleepStageType.REM,
)
health.writeData(
records = listOf(
SleepSessionRecord(
startTime = startTime,
endTime = endTime,
stages = List(6) {
SleepSessionRecord.Stage(
startTime = startTime.plus((10 * it).minutes),
endTime = startTime.plus((10 * it).minutes + 10.minutes),
type = types[it],
)
},
)
)
)
health.writeData(
records = listOf(
StepsRecord(
startTime = Clock.System.now().minus(1.days).minus(3.hours),
endTime = Clock.System.now().minus(1.days).minus(1.hours),
count = 75,
),
StepsRecord(
startTime = Clock.System.now().minus(1.hours),
endTime = Clock.System.now(),
count = 123,
),
)
)
There are different supported Mass
units: kilograms, ounces, pounds, grams, milligrams, micrograms.
health.writeData(
records = listOf(
// Weight in kilograms
WeightRecord(
time = Clock.System.now().minus(1.days),
weight = Mass.kilograms(61.2),
),
// Weight in pounds
WeightRecord(
time = Clock.System.now(),
weight = Mass.pounds(147.71),
),
)
)