devShakib

vitals

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.

vitals

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 sheet
is 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 values
land 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.

Why another one

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.

The three permission states

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}

Statistics, not raw samples

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.

Testing

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

iOS setup

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.

Android setup

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.

License

MIT © K M Shahriar Hossain

vitals on pub.dev