Skip to content

Data types & capabilities ​

HealthDataType is the entry point for almost everything you do. It is not just an enum of names — it is where permissions, requests, and capabilities come from.

One constant, four jobs ​

dart
// 1. Permissions derive from the type.
HealthDataType.steps.readPermission
HealthDataType.steps.writePermission

// 2. Read requests derive from the type — and carry it into the response.
HealthDataType.steps.readInTimeRange(startTime: from, endTime: to)
HealthDataType.steps.readById(HealthRecordId('...'))

// 3. Delete requests too.
HealthDataType.steps.deleteByIds([id])
HealthDataType.steps.deleteInTimeRange(startTime: from, endTime: to)

// 4. And aggregates — only the ones this type supports.
HealthDataType.steps.aggregateSum(startTime: from, endTime: to)

Because permissions and requests both come from the same constant, they cannot drift apart. There is no way to request READ_STEPS and then accidentally query weight.

Capabilities are encoded in the type ​

Each data type implements only the interfaces matching what it can actually do. That has a concrete effect: HealthDataType.weight.aggregateSum(...) does not exist, because summing body weight is meaningless. Average, minimum, and maximum do exist. The compiler stops the nonsensical call rather than the platform rejecting it at runtime.

The same mechanism covers writes. A type marked @readOnly — typically a metric the OS computes for you — offers no write path, and attempting one throws UnsupportedOperationException.

CapabilityWhere it comes fromFailure mode if unsupported
ReadEvery data type—
WriteType is not @readOnlyCompile error, or UnsupportedOperationException
Aggregate (Sum/Avg/Min/Max/Duration)Per-type aggregation interfacesThe method is not offered
UpdateAndroid onlyUnsupportedOperationException on iOS
DeleteRecords your app wroteAuthorizationException for other apps' records

Which aggregations each type supports is searchable in the health data type explorer — filter by Aggregatable only to see the 112 types that support at least one metric.

Platform availability is per type ​

Not every conceptual measurement exists on both stores. Of the 143 typed data types, some are Health Connect-only, many nutrient types are exposed as discrete HealthKit identifiers while Health Connect models them as fields on a single NutritionRecord, and a few require a specific iOS version.

Each type exposes its runtime requirements:

dart
final requirements = HealthDataType.infrequentMenstrualCycleEvent
    .healthPlatformRequirements;
// [AppleHealthRequirement.ios16OrLater]

This type is supported on iOS 16 or later. Calling it on Android or iOS 15 throws UnsupportedOperationException. Pass the list to getSupportStatusFor() before presenting an operation.

Reads stay typed all the way through ​

This is what makes the type parameter worth the ceremony:

dart
final response = await connector.readRecords(
  HealthDataType.weight.readInTimeRange(startTime: from, endTime: to),
);

// List<WeightRecord> — `weight` is a Mass, no cast needed.
for (final record in response.records) {
  print(record.weight.inKilograms);
}

No as, no is check, no null-guarded cast. If you change the data type on the first line, every downstream line that no longer type-checks becomes a compile error — which is exactly when you want to find out.

The record shape follows the type too. HealthDataType.heartRate yields HeartRateRecord, an instant record with a single rate; HealthDataType.heartRateSeries yields HeartRateSeriesRecord, which carries samples. You get the right shape without asking for it:

dart
// Instant record — one reading.
final single = await connector.readRecords(
  HealthDataType.heartRate.readInTimeRange(startTime: from, endTime: to),
);
for (final record in single.records) {
  print(record.rate.inPerMinute);
}

// Series record — many samples inside one record.
final series = await connector.readRecords(
  HealthDataType.heartRateSeries.readInTimeRange(startTime: from, endTime: to),
);
for (final record in series.records) {
  for (final sample in record.samples) {
    print(sample.rate.inPerMinute);
  }
}

Static typing does not cover platform availability

Those two heart-rate types are also a platform split — heartRate is HealthKit-only and heartRateSeries is Health Connect-only. That constraint is not enforced by the compiler: passing an unsupported type compiles fine and throws UnsupportedOperationException at runtime. Resolve each type's healthPlatformRequirements with getSupportStatusFor() before using it.

Finding the type you need ​

Released under the MIT License.