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 the annotations that describe platform constraints. 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 marks the platform-specific parts:

dart
@supportedOnHealthConnect
Future<void> updateRecord(HealthRecord record);

The annotation tells you — and, in future, a custom lint rule — that this call has no iOS implementation and will throw UnsupportedOperationException there. You get the capability where it exists, plus a clear signal about where it does not, instead of an API that silently pretends the platforms match.

The same principle governs data types: a type that only Health Connect models is still available, annotated, 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.
  • Do branch on HealthConnector.healthPlatform for annotated APIs, or catch UnsupportedOperationException.
  • Never call anything marked @internalUse; those factories exist for the adapters.

Released under the Apache 2.0 License.