Skip to content

Repository files navigation

Vitality

Build Maven Central License

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.

Installation

dependencies {
    implementation("io.github.crowded-libs:vitality:0.2.1")
}

Requirements:

  • iOS 13.0+ with the HealthKit capability. Newer data types degrade gracefully: each is gated on the OS version that introduced it (14/16/17/18/26).
  • Android API 26+ with Health Connect installed. Clinical records require Android 16 (API 35). Mindfulness and skin temperature depend on the installed Health Connect version and are checked at runtime.

Platform setup

Android

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 -->

iOS

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
)

Quick start

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") }
}

Permission semantics:

  • Android: granular per-record-type permissions; checkPermissions reflects actual grants.
  • iOS: HealthKit reports write (share) authorization but never discloses read authorization. requestPermissions reports write denials accurately and sets readPermissionsIndeterminate=true in platformSpecificInfo; checkPermissions returns NotDetermined for read-only sets.

Data type support

Column values are the native type backing each HealthDataType. "No" means the platform has no equivalent; reads fail with UnsupportedFeature.

Fitness

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

Energy: Calories, ActiveCalories, and BasalCalories are distinct native types — not merged. On CalorieData, use totalCalories for Android totals, activeCalories for active energy, and basalCalories for basal. Android basal is a kcal/day BMR rate (metadata["basalUnit"] = "kilocaloriesPerDay"); iOS basal is interval energy in kcal. Do not add a BMR rate to active energy to invent a total.

Vitals

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

Body measurements

Vitality type HealthKit (iOS) Health Connect (Android)
Weight bodyMass WeightRecord
Height height HeightRecord
BMI bodyMassIndex No
BodyFat bodyFatPercentage BodyFatRecord
LeanBodyMass leanBodyMass LeanBodyMassRecord

Nutrition

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

Sleep and mindfulness

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

Mobility (iOS 14+ only)

WalkingAsymmetry, WalkingDoubleSupportPercentage, WalkingSpeed, WalkingStepLength, StairAscentSpeed, StairDescentSpeed, SixMinuteWalkTestDistance, NumberOfTimesFallen, StandHours — HealthKit identifiers, read-only / device-generated. Not in Health Connect.

Environmental (iOS only)

EnvironmentalAudioExposure, HeadphoneAudioExposure, UVExposure, TimeInDaylight (iOS 17+) — HealthKit identifiers. Not in Health Connect.

Reproductive health

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

Clinical records (FHIR)

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)

Methods: readImmunizations, readMedications, readAllergies, readConditions, readLabResults, readProcedures, areClinicalRecordsAvailable. Parsed types: FHIRImmunization, FHIRMedicationStatement, FHIRMedicationRequest, FHIRAllergyIntolerance, FHIRCondition, FHIRObservation, FHIRProcedure. iOS: HKClinicalRecord FHIR payloads. Android: Personal Health Record API (FHIR R4).

User-logged medications (iOS 26+, distinct from FHIR clinical meds): readUserMedications, readMedicationDoseEvents, areUserMedicationsAvailable. Elsewhere: UnsupportedFeature.

Real-time monitoring

observe(dataType, samplingInterval) returns a Flow of typed model instances. Unsupported or OS-gated types fail with UnsupportedFeature (they do not complete empty).

  • iOS: HKObserverQuery push; samplingInterval is ignored.
  • Android: Health Connect Changes API — initial snapshot, then 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") }

Workouts

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 (HealthDataType.Elevation).

Statistics

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.

License

Apache License 2.0. See LICENSE.

About

A Kotlin Multiplatform library providing unified health data access across Apple HealthKit (iOS) and Health Connect (Android). Vitality offers comprehensive health metrics, real-time monitoring, workout management, and medical records support through FHIR standards.

Resources

Stars

19 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages