health_connect_reporter

CI

About

A Flutter plugin that reads, writes and observes Android Health Connect data. It wraps the Kotlin library HealthConnectReporter and hands you its payloads as plain Dart models.

Its siblings are health_kit_reporter for Apple Health and samsung_health_reporter for Samsung Health.

Status: every HealthConnectReporter call exists; nothing is released yet.

Requirements

  • Android 8 (API 26) or later. Health Connect is part of Android 14+; on Android 9–13 the user installs the Health Connect app from Google Play.
  • Flutter 3.44+, Java 17.

Installation

dependencies:
  health_connect_reporter: ^1.0.0 # x-release-please-version

The plugin resolves HealthConnectReporter from JitPack, so add the repository to android/build.gradle.kts:

allprojects {
    repositories {
        google()
        mavenCentral()
        maven("https://jitpack.io")
    }
}

Manifest

Your app declares every health permission it requests, for example:

<uses-permission android:name="android.permission.health.READ_STEPS" />
<uses-permission android:name="android.permission.health.WRITE_STEPS" />

It also needs an activity that shows your privacy policy for the androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE intent and, on Android 14+, an activity-alias for android.intent.action.VIEW_PERMISSION_USAGE with the android.intent.category.HEALTH_PERMISSIONS category. Google Play requires a Health Connect permissions declaration before you publish.

Usage

switch (await HealthConnectReporter.availability()) {
  case Availability.available:
    // read, write and observe
    break;
  case Availability.updateRequired:
    await HealthConnectReporter.openInstall();
    break;
  case Availability.unavailable:
    // Health Connect does not run on this device
    break;
}

Permissions

The permission screen runs on your app's activity, so a plain FlutterActivity works. Request only permissions your manifest declares: anything else fails with InvalidValue, and so does an empty set.

final granted = await HealthConnectReporter.requestPermissions({
  ...Permission.of([HealthType.steps, HealthType.heartRate]), // read and write
  const PermissionData(healthType: HealthType.weight, access: AccessType.read),
  const PermissionReadInBackground(), // needs Feature.readHealthDataInBackground
});
final current = await HealthConnectReporter.grantedPermissions();
await HealthConnectReporter.revokeAllPermissions();

A permission that needs a feature the device lacks fails with NotAvailable; check features first:

if (await HealthConnectReporter.isFeatureAvailable(Feature.skinTemperature)) {
  // read and write HealthType.skinTemperature
}

openSettings() opens Health Connect's data management screen, and openInstall() opens Health Connect on Google Play when availability() is Availability.updateRequired.

Read and aggregate

final range = TimeRange.lastDays(7);
final samples = await HealthConnectReporter.read(HealthType.heartRate, range,
    ordering: Ordering.ascending, limit: 500);
for (final sample in samples.whereType<HeartRate>()) {
  print(sample.harmonized.series.map((e) => e.beatsPerMinute));
}

// one page at a time
var page = await HealthConnectReporter.readPage(HealthType.steps, range, pageSize: 100);
while (page.nextPageToken != null) {
  page = await HealthConnectReporter.readPage(HealthType.steps, range,
      pageSize: 100, pageToken: page.nextPageToken);
}

final weight = await HealthConnectReporter.readById(HealthType.weight, 'record-id');

final steps = await HealthConnectReporter.aggregate(Aggregation.stepsCountTotal, range,
    origins: {const DataOrigin(packageName: 'com.google.android.apps.fitness')});
final daily = await HealthConnectReporter.aggregateGroups(
    {Aggregation.stepsCountTotal, Aggregation.heartRateBpmAvg},
    range,
    TimeGroupPeriod(unit: TimeGroupUnit.daily, zoneId: 'Europe/Berlin'));

Values come in the library's canonical units (kg, kcal, m, m/s, W, mmHg, °C, L, mmol/L, percent; durations in milliseconds). Without PermissionReadHistory() Health Connect returns data from 30 days before the first grant onwards, and a type that needs a missing feature fails with NotAvailable.

Write

Every Health Connect record is writable by the app that holds its write permission. A sample without a clientRecordId gets a random one on insert, so you can update and delete it later without reading it back.

final now = DateTime.now().millisecondsSinceEpoch;
final inserted = await HealthConnectReporter.insert([
  Steps(
    startTimestamp: now - 3600000,
    endTimestamp: now,
    harmonized: const StepsHarmonized(count: 1200),
    clientRecordId: 'steps-2026-10-10-09',
    clientRecordVersion: 1,
  ),
  Weight(startTimestamp: now, harmonized: const WeightHarmonized(weight: 72.5)),
]);
print(inserted.map((e) => e.id)); // Health Connect ids, in input order

// a higher clientRecordVersion replaces the record with that clientRecordId
await HealthConnectReporter.update([
  Steps(
    startTimestamp: now - 3600000,
    endTimestamp: now,
    harmonized: const StepsHarmonized(count: 1300),
    clientRecordId: 'steps-2026-10-10-09',
    clientRecordVersion: 2,
  ),
]);

await HealthConnectReporter.delete(inserted.last); // by id, else by clientRecordId
await HealthConnectReporter.deleteByIds(HealthType.steps, clientRecordIds: ['steps-2026-10-10-09']);
await HealthConnectReporter.deleteByRange(HealthType.weight, TimeRange.lastDays(1));

Observe changes

An anchor is a position in Health Connect's change history. Store it, and read only what changed since:

var anchor = await HealthConnectReporter.anchor({HealthType.steps, HealthType.heartRate});
// later
final changeSet = await HealthConnectReporter.changes(anchor);
print('${changeSet.upserted.length} upserted, ${changeSet.deletedIds.length} deleted');
anchor = changeSet.anchor; // store it for the next call

While the app runs, observe polls on an event channel of its own, first right away, and reports every non-empty change set:

final subscription = HealthConnectReporter.observe(
  anchor,
  interval: const Duration(minutes: 1),
  onUpdate: (changeSet) => anchor = changeSet.anchor,
  onError: (error) => print(error), // polling stops, e.g. on AnchorExpired
);
// cancel to stop polling
await subscription.cancel();

Health Connect keeps changes for 30 days: an older anchor fails with AnchorExpired, so read the data again in full and take a new anchor.

Background sync

observe polls only while the Flutter engine runs. To sync in the background, schedule a periodic task with workmanager and read the changes of your stored anchor there; reading in the background needs PermissionReadInBackground() (check Feature.readHealthDataInBackground first) and its manifest entry android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND.

@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, input) async {
    final anchor = Anchor(token: await loadToken()); // your storage
    final changeSet = await HealthConnectReporter.changes(anchor);
    await upload(changeSet); // your sync
    await saveToken(changeSet.anchor.token);
    return true;
  });
}

// once, after the user granted the permissions
await Workmanager().initialize(callbackDispatcher);
await Workmanager().registerPeriodicTask('health-sync', 'health-sync',
    frequency: const Duration(minutes: 15));

Errors arrive as PlatformExceptions whose code is the method name and whose details name the error: NotAuthorized, InvalidType, InvalidValue, NotAvailable, AnchorExpired or Platform.

Example

example/ lists every plugin method, grouped like the library (Manager, Reader, Aggregates, Writer, Observer); tap a row to run it. Its manifest declares every health permission, the permissions-rationale activity and the VIEW_PERMISSION_USAGE alias, so it doubles as a setup reference.

example/integration_test/catalog_test.dart runs every row on an Android 14+ phone. Grant the permissions once from the app's requestPermissions row, then:

cd example
flutter test integration_test --no-uninstall -d <device-id>

License

MIT

Libraries

exceptions
health_connect_reporter
model/aggregate
model/anchor
model/availability
model/change_set
model/data_origin
model/decorator/extensions
model/decorator/json
model/decorator/platform
model/decorator/subscription
model/device
model/page
model/payload/active_calories_burned
model/payload/basal_body_temperature
model/payload/basal_metabolic_rate
model/payload/blood_glucose
model/payload/blood_pressure
model/payload/body_fat
model/payload/body_temperature
model/payload/body_water_mass
model/payload/bone_mass
model/payload/cervical_mucus
model/payload/cycling_pedaling_cadence
model/payload/distance
model/payload/elevation_gained
model/payload/exercise_session
model/payload/floors_climbed
model/payload/heart_rate
model/payload/heart_rate_variability_rmssd
model/payload/height
model/payload/hydration
model/payload/intermenstrual_bleeding
model/payload/lean_body_mass
model/payload/menstruation_flow
model/payload/menstruation_period
model/payload/mindfulness_session
model/payload/nutrition
model/payload/ovulation_test
model/payload/oxygen_saturation
model/payload/payload
model/payload/planned_exercise_session
model/payload/power
model/payload/respiratory_rate
model/payload/resting_heart_rate
model/payload/sample
model/payload/sexual_activity
model/payload/skin_temperature
model/payload/sleep_session
model/payload/speed
model/payload/steps
model/payload/steps_cadence
model/payload/total_calories_burned
model/payload/vo2_max
model/payload/weight
model/payload/wheelchair_pushes
model/permission
model/time_group
model/time_range
model/type/access_type
model/type/aggregation
model/type/blood_pressure_measurement_location
model/type/body_position
model/type/body_temperature_measurement_location
model/type/cervical_mucus_appearance
model/type/cervical_mucus_sensation
model/type/device_type
model/type/exercise_phase
model/type/exercise_segment_type
model/type/exercise_type
model/type/feature
model/type/health_type
model/type/meal_type
model/type/menstruation_flow_type
model/type/mindfulness_session_type
model/type/ordering
model/type/ovulation_test_result
model/type/protection_used
model/type/recording_method
model/type/relation_to_meal
model/type/skin_temperature_measurement_location
model/type/sleep_stage_type
model/type/specimen_source
model/type/time_group_unit
model/type/value_enum
model/type/vo2_max_measurement_method