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