Skip to content

Architecture ​

Health Connector SDK is a facade over two native health stores. Understanding the layers explains why some things are uniform and others are explicitly not.

The layers ​

Layer diagram. Your Flutter app calls the health_connector facade, which builds on the shared health_connector_core vocabulary and delegates to one of two platform adapters: health_connector_hc_android over Kotlin and Health Connect, or health_connector_hk_ios over Swift and HealthKit. Each adapter communicates with native code over a generated Pigeon channel.

Core defines the vocabulary: record models, measurement units, permission types, exceptions, and runtime platform requirements. It contains no platform code, which is why the same WeightRecord means the same thing everywhere.

Platform adapters translate that vocabulary into native calls. Each owns its Pigeon-generated channel and a set of mappers — toDto() on the way down, toDomain() on the way back.

The facade is what you import. HealthConnector.create() detects the platform and returns the matching client; everything after that is delegation.

Why a facade rather than a common denominator ​

A "lowest common denominator" API would have to drop updateRecord() entirely, because HealthKit cannot do it. Health Connector SDK instead exposes the full union of capabilities and checks platform-specific parts at runtime:

dart
Future<void> updateRecord(HealthRecord record);

This call has no iOS implementation and throws UnsupportedOperationException there. Data types and features expose healthPlatformRequirements so callers can check platform and version support before executing an operation.

The same principle governs data types: a type that only Health Connect models is still available, reports a Health Connect-only runtime requirement, and throws on iOS rather than being omitted from the SDK.

What crosses the channel ​

Requests do not travel as records. They travel as DTOs generated by Pigeon, so the boundary is type-checked on both sides at build time rather than being a Map<String, dynamic> contract you have to keep in sync by hand.

text
StepsRecord            (your code)
  → StepsRecordDto     (toDto(), Dart side)
  → StepsRecordDto     (Pigeon-generated Kotlin/Swift)
  → StepsRecord        (native record type)

A practical consequence: unit conversions happen in Dart, before the channel. Mass.pounds(155) becomes the platform's expected unit during mapping, so a unit mistake is impossible to introduce at the boundary.

Native logs come back to Dart ​

Diagnostics from the Kotlin and Swift layers are forwarded through the same channel into your Dart log processors. There is one control plane for SDK output rather than three — Dart, Logcat, and Console — which is what makes the zero-logging default meaningful. See Configure logging.

What this means for your code ​

  • Import only package:health_connector/health_connector.dart. The platform packages are transitive.
  • Do not branch on platform for ordinary reads, writes, and aggregates — they are uniform.
  • Resolve an API's healthPlatformRequirements with getSupportStatusFor(), and retain an UnsupportedOperationException fallback.
  • Never call anything marked @internalUse; those factories exist for the adapters.

Released under the MIT License.