vitality
0.1.1indexedOffers unified access to health data, supporting over 66 health metrics, real-time monitoring, workout management, and FHIR medical records compliance with cross-platform API for seamless integration.
Offers unified access to health data, supporting over 66 health metrics, real-time monitoring, workout management, and FHIR medical records compliance with cross-platform API for seamless integration.
A Kotlin Multiplatform library exposing Apple HealthKit (iOS) and Android Health Connect through one API. The common surface is the union of the two platforms' data types, mapped 1:1 to the native records with no additional abstraction; operations return Result<T> and real-time data is delivered as Kotlin Flows. Types one platform cannot serve fail with UnsupportedFeature rather than returning empty data.
dependencies {
implementation("io.github.crowded-libs:vitality:0.2.1")
}
Requirements:
Initialize the context provider in your Application class and register the activity used for permission requests:
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
HealthConnectorContextProvider.initialize(applicationContext)
}
}
// In your Activity (before onCreate completes), either:
val connector = createHealthConnector(activity)
// or set HealthConnectorContextProvider.activity = this and use createHealthConnector().
// Call HealthConnectorContextProvider.clearActivity() from onDestroy().
Declare the Health Connect permissions your app uses in AndroidManifest.xml:
<uses-permission android:name="android.permission.health.READ_HEART_RATE" />
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE" />
<!-- one entry per record type you read or write -->
Enable the HealthKit capability and add usage descriptions to Info.plist:
<key>NSHealthShareUsageDescription</key>
<string>This app reads your health data to provide insights</string>
<key>NSHealthUpdateUsageDescription</key>
<string>This app updates your health data from workouts</string>
Background delivery is iOS-only (no Health Connect equivalent). Cast to HealthKitConnector:
(connector as? HealthKitConnector)?.enableBackgroundDelivery(
dataTypes = setOf(HealthDataType.HeartRate),
updateFrequency = BackgroundDeliveryFrequency.IMMEDIATE
)
Permission semantics:
checkPermissions reflects actual grants.requestPermissions reports write denials accurately and sets readPermissionsIndeterminate=true in ; returns for read-only sets.Column values are the native type backing each HealthDataType. "No" means the platform has no equivalent; reads fail with UnsupportedFeature.
Energy: Calories, ActiveCalories, and are distinct native types — not merged. On , use for Android totals, for active energy, and for basal. Android basal is a (); iOS basal is . Do not add a BMR rate to active energy to invent a total.
WalkingAsymmetry, WalkingDoubleSupportPercentage, WalkingSpeed, WalkingStepLength, StairAscentSpeed, StairDescentSpeed, , , — HealthKit identifiers, read-only / device-generated. Not in Health Connect.
EnvironmentalAudioExposure, HeadphoneAudioExposure, UVExposure, TimeInDaylight (iOS 17+) — HealthKit identifiers. Not in Health Connect.
Methods: , , , , , , . Parsed types: , , , , , , . iOS: FHIR payloads. Android: Personal Health Record API (FHIR R4).
User-logged medications (iOS 26+, distinct from FHIR clinical meds): readUserMedications, readMedicationDoseEvents, areUserMedicationsAvailable. Elsewhere: UnsupportedFeature.
observe(dataType, samplingInterval) returns a Flow of typed model instances. Unsupported or OS-gated types fail with UnsupportedFeature (they do not complete empty).
HKObserverQuery push; samplingInterval is ignored.getChanges(token) every samplingInterval (default 30s). Each record once; token expiry re-baselines; transient failures retry with backoff; permission failures fail the flow.connector.observe<HeartRateData>(HealthDataType.HeartRate)
.collect { println("${it.timestamp}: ${it.bpm} bpm") }
val session = connector.startWorkoutSession(WorkoutType.RUNNING).getOrThrow()
launch { session.observeHeartRate().collect { println("HR: ${it.bpm}") } }
launch { session.observeDistance().collect { println("distance: ${it.distance} m") } }
session.pause()
session.resume()
session.end() // persists (Android records pauses as ExerciseSegments)
// or connector.discardWorkoutSession(session.id)
Read with readWorkouts(start, end); write with writeWorkout(workoutData). WorkoutData.segments mirrors Health Connect ExerciseSegment (Android-only; ignored on iOS write). Heart-rate min/avg/max on WorkoutData are summaries only — they are not expanded into sample series on write; write real samples via writeHealthData. Elevation is also a first-class type on Android ().
readStatistics aggregates over a range. Without bucketDuration, use values; with it, use buckets.
val now = Clock.System.now()
val steps = connector.readStatistics(
dataType = HealthDataType.Steps,
startDate = now - 1.days,
endDate = now,
statisticOptions = setOf(StatisticOption.SUM)
).getOrThrow()
println("steps: ${steps.values[StatisticOption.SUM]}")
Options: MINIMUM, MAXIMUM, AVERAGE, SUM. Cumulative types support SUM; discrete types support min/max/average. Incompatible combinations fail with InvalidDataException.
Apache License 2.0. See LICENSE.
import vitality.*
import vitality.models.*
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
val connector = createHealthConnector()
suspend fun example() {
connector.initialize().getOrThrow()
val permissions = setOf(
HealthPermission(HealthDataType.HeartRate, HealthPermission.AccessType.READ),
HealthPermission(HealthDataType.Steps, HealthPermission.AccessType.READ),
)
val grantResult = connector.requestPermissions(permissions).getOrThrow()
println("granted=${grantResult.granted.size} denied=${grantResult.denied.size}")
val now = Clock.System.now()
connector.readHealthData(HealthDataType.HeartRate, now - 1.days, now)
.onSuccess { points ->
points.filterIsInstance<HeartRateData>().forEach { println("${it.timestamp}: ${it.bpm} bpm") }
}
connector.observe<HeartRateData>(HealthDataType.HeartRate).collect { println("live: ${it.bpm} bpm") }
}
platformSpecificInfocheckPermissionsNotDetermined| Vitality type | HealthKit (iOS) | Health Connect (Android) |
|---|
Steps | stepCount | StepsRecord |
Distance | distanceWalkingRunning | DistanceRecord |
Calories | activeEnergyBurned | TotalCaloriesBurnedRecord |
ActiveCalories | activeEnergyBurned | ActiveCaloriesBurnedRecord |
BasalCalories | basalEnergyBurned (interval kcal) | BasalMetabolicRateRecord (kcal/day rate) |
Floors | flightsClimbed | FloorsClimbedRecord |
Elevation | No | ElevationGainedRecord |
Workout | HKWorkoutType | ExerciseSessionRecord |
VO2Max | vo2Max | Vo2MaxRecord |
WheelchairPushes | No | WheelchairPushesRecord |
Speed | runningSpeed (iOS 16+) | SpeedRecord |
CyclingCadence | cyclingCadence (iOS 17+) | CyclingPedalingCadenceRecord |
Power | cyclingPower (iOS 17+) | PowerRecord (any activity) |
CyclingFunctionalThresholdPower | cyclingFunctionalThresholdPower (iOS 17+) | No |
RunningStrideLength | runningStrideLength (iOS 16+) | No |
RunningVerticalOscillation | runningVerticalOscillation (iOS 16+) | No |
RunningGroundContactTime | runningGroundContactTime (iOS 16+) | No |
WorkoutEffortScore | workoutEffortScore (iOS 18+) | No |
EstimatedWorkoutEffortScore | estimatedWorkoutEffortScore (iOS 18+, read-only) | No |
BasalCaloriesCalorieDatatotalCaloriesactiveCaloriesbasalCaloriesmetadata["basalUnit"] = "kilocaloriesPerDay"| Vitality type | HealthKit (iOS) | Health Connect (Android) |
|---|
HeartRate | heartRate | HeartRateRecord |
HeartRateVariability | heartRateVariabilitySDNN (fills sdnn) | HeartRateVariabilityRmssdRecord (fills rmssd) |
RestingHeartRate | restingHeartRate | RestingHeartRateRecord |
BloodPressure | blood pressure correlation | BloodPressureRecord |
RespiratoryRate | respiratoryRate | RespiratoryRateRecord |
OxygenSaturation | oxygenSaturation | OxygenSaturationRecord |
BodyTemperature | bodyTemperature | BodyTemperatureRecord |
WristTemperature | appleSleepingWristTemperature (iOS 16+, read-only) | SkinTemperatureRecord (feature-gated) |
BloodGlucose | bloodGlucose | BloodGlucoseRecord |
PeripheralPerfusionIndex | peripheralPerfusionIndex | No |
IrregularHeartRhythmEvent | irregularHeartRhythmEvent | No |
Electrocardiogram | HKElectrocardiogram (iOS 14+) | No |
| Vitality type | HealthKit (iOS) | Health Connect (Android) |
|---|
Weight | bodyMass | WeightRecord |
Height | height | HeightRecord |
BMI | bodyMassIndex | No |
BodyFat | bodyFatPercentage | BodyFatRecord |
LeanBodyMass | leanBodyMass | LeanBodyMassRecord |
| Vitality type | HealthKit (iOS) | Health Connect (Android) |
|---|
Water | dietaryWater | HydrationRecord |
Protein | dietaryProtein | NutritionRecord.protein |
Carbohydrates | dietaryCarbohydrates | NutritionRecord.totalCarbohydrate |
Fat | dietaryFatTotal | NutritionRecord.totalFat |
Fiber | dietaryFiber | NutritionRecord.dietaryFiber |
Sugar | dietarySugar | NutritionRecord.sugar |
Caffeine | dietaryCaffeine | NutritionRecord.caffeine |
| Vitality type | HealthKit (iOS) | Health Connect (Android) |
|---|
Sleep | sleepAnalysis | SleepSessionRecord (with stages) |
SleepApneaEvent | sleepApneaEvent (iOS 18+) | No |
Mindfulness | mindfulSession | MindfulnessSessionRecord (feature-gated) |
StateOfMind | HKStateOfMind (iOS 18+, read + write) | No |
SixMinuteWalkTestDistanceNumberOfTimesFallenStandHours| Vitality type | HealthKit (iOS) | Health Connect (Android) |
|---|
MenstruationFlow | menstrualFlow | MenstruationFlowRecord |
MenstruationPeriod | No | MenstruationPeriodRecord |
IntermenstrualBleeding | intermenstrualBleeding | IntermenstrualBleedingRecord |
CervicalMucus | cervicalMucusQuality | CervicalMucusRecord |
OvulationTest | ovulationTestResult | OvulationTestRecord |
SexualActivity | sexualActivity | SexualActivityRecord |
| Vitality type | HealthKit (iOS 12+) | Health Connect (Android 16+) |
|---|
ClinicalAllergies | allergyRecord | MedicalResource (allergies/intolerances) |
ClinicalConditions | conditionRecord | MedicalResource (conditions) |
ClinicalImmunizations | immunizationRecord | MedicalResource (vaccines) |
ClinicalLabResults | labResultRecord | MedicalResource (laboratory results) |
ClinicalMedications | medicationRecord | MedicalResource (medications) |
ClinicalProcedures | procedureRecord | MedicalResource (procedures) |
ClinicalVitalSigns | vitalSignRecord | MedicalResource (vital signs) |
readImmunizationsreadMedicationsreadAllergiesreadConditionsreadLabResultsreadProceduresareClinicalRecordsAvailableFHIRImmunizationFHIRMedicationStatementFHIRMedicationRequestFHIRAllergyIntoleranceFHIRConditionFHIRObservationFHIRProcedureHKClinicalRecordHealthDataType.ElevationSurfaced from shared tags and platforms — no rankings paid for.