Type-safe access to Apple Health and Health Connect. Bucketed statistics computed on-device, permissions modelled honestly, and an in-memory fake so health flows are testable.
Type-safe access to Apple Health and Health Connect.
final steps = await Vitals.instance.read( VitalType.steps, from: weekAgo, to: now,);// List<CountSample> — steps.first.count is an int.
Status — read this before writing data.
Reading is verified. The iOS path is exercised against a real HealthKit
store, and the Dart contract has a full unit-test suite behind it.
Writing is not verified on either platform. The code is complete and
compiles, but no write has yet been observed to succeed end to end: on the
iOS Simulator save returns Not authorized even after the permission sheetis granted, and the Android round trip has not been run. It is unclear
whether that is a Simulator limitation or a defect here.
So: use 0.1.0 for reading and aggregating. If you write, verify the valuesland correctly in Health or Health Connect before trusting it — a unit-
conversion mistake would be silent, and health records are awkward to
correct. Reports from real devices are very welcome.
health is the established package, it is actively maintained, and it covers
more data types than this does. If you need breadth today, use it. This exists
for three things it does not do.
Nothing is cast. The type you ask for decides the type you get back:
final weight = await vitals.read(VitalType.bodyMass, from: a, to: b);weight.first.value.pounds; // a Mass, not a double you hope is kilograms
Permissions are modelled on what the platforms can actually answer. There
is deliberately no readAccess(), because HKAuthorizationStatus has exactly
three values and all three describe writing:
HKAuthorizationStatusNotDeterminedHKAuthorizationStatusSharingDeniedHKAuthorizationStatusSharingAuthorized
Apple omits read status on purpose — knowing which health types someone has
recorded would itself disclose health information. So any API that answers
"do I have read permission?" is guessing on iOS. This one doesn't ask.
Health flows are testable. FakeVitals runs the entire API in memory.
Running against a real store turned up something worth encoding: HealthKit
distinguishes never asked from asked and refused, and only the second is
silent.
| State | What a read does |
| --- | --- |
| Never requested | throws AuthorizationNotDeterminedException |
| Requested, granted | returns data |
| Requested, refused | returns empty — indistinguishable from no data |
So the exception means your app forgot to call requestPermissions, which is a
bug you can fix. Emptiness afterwards is genuinely ambiguous, on every package,
forever. Write your UI accordingly:
switch (await vitals.readAccessOnAndroid({VitalType.steps})) { case null: // iOS: unknowable, attempt the read and handle empty case final access: // Android: an actual answer}A year of heart-rate readings is hundreds of thousands of points. Reducing them
on the platform beats shipping them across the channel:
final daily = await vitals.statistics( VitalType.steps, from: monthAgo, to: now, bucket: VitalBucket.daily,);
Each type knows how it should be reduced — steps sum, heart rate averages,
weight takes the latest — so you rarely pass aggregate yourself. A bucket
with no samples reports null, never 0; conflating those is how averages go
wrong.
final vitals = FakeVitals() ..seedCounts(VitalType.steps, { DateTime(2026, 8, 24): 8210, DateTime(2026, 8, 25): 11430, });It models the awkward states too, not just the happy path:
FakeVitals(available: false); // ineligible deviceFakeVitals(permissionSheetSucceeds: false); // user dismissed the sheetFakeVitals(readsAreBlocked: true); // the iOS silent denial
Three things, all required, none of them optional:
1. Add the HealthKit capability in Xcode, or an entitlements file:
<key>com.apple.developer.healthkit</key><true/>
2. Add both usage descriptions to Info.plist. Omitting either crashes the
app the moment you request that kind of access:
<key>NSHealthShareUsageDescription</key><string>Why you read health data.</string><key>NSHealthUpdateUsageDescription</key><string>Why you write health data.</string>
3. Minimum iOS 15. Sleep stages finer than "asleep" and workout totals need
iOS 16; below that they degrade rather than failing.
Health Connect requires minSdk 26, and the permissions your app uses must
be declared in AndroidManifest.xml. Health Connect is built in from Android
14; older devices need it installed from the Play Store, which
isAvailable() reports.
Two types are unsupported here and say so rather than returning something
close: mindfulSession, which Health Connect does not model, and
basalEnergyBurned, whose nearest record is a rate rather than an interval
total.
MIT © K M Shahriar Hossain