Health Data in Flutter: HealthKit, Google Fit and Bluetooth devices
Reading health data from three sources and turning it into one coherent model — permissions on each platform, de-duplication, Bluetooth device realities, and what App Store review checks.
Health apps look simple from the outside: show the patient their numbers. The complexity is that the numbers arrive from places that disagree with each other. Apple HealthKit on iOS, Google Fit on Android, and a Bluetooth glucometer that knows nothing about either.
This guide covers what that integration involves in a Flutter app: normalising the data, handling permissions on each platform, dealing with Bluetooth devices, and passing App Store review.
The core problem: three sources, one timeline
Each source has its own idea of what a reading is. Units differ. Timestamps differ in precision and time zone. A platform may return many samples where the user took one measurement. The same reading can arrive twice, once from the device and once after the platform syncs it.
So the first decision matters more than any package choice: normalise at the boundary. Define your own reading model and convert everything into it the moment it arrives.
class Reading {
final ReadingType type; // glucose, weight, steps, bp
final double value; // always in your canonical unit
final DateTime takenAtUtc; // always UTC
final ReadingSource source; // healthKit, googleFit, device, manual
final String? externalId; // for de-duplication
}Nothing above this layer should know that HealthKit exists. When you later add a fourth source, only the adapter changes.
Permissions are not one thing
This is where most of the lost time goes.
- iOS / HealthKit: permission is per data type, and read and write are separate. Critically, iOS never tells you that read access was denied.You get an empty result, which is indistinguishable from “no data”. You cannot show “please grant access” based on the API alone; you have to track whether you have ever received data and guide the user accordingly.
- Android: the fitness platform has its own consent screen tied to a Google account, plus runtime permissions. Newer Android versions also separate body-sensor and activity-recognition permissions.
- Bluetooth: a separate set again, and on Android it has historically been entangled with location permission, which alarms users unless you explain why.
Ask for each permission at the moment it is needed, with one sentence of context, not all at once on first launch. Acceptance rates and review outcomes both improve.
Bluetooth devices behave like the physical world
A glucometer is not an API. It is a device that is off most of the time, wakes up briefly, may be out of range, may have a flat battery, and may hand you readings from last Tuesday in a batch.
Design for that:
- Sync is a background job, not a screen.The user should not have to sit on a “connecting” spinner.
- Expect batches and duplicates. De-duplicate on device id plus timestamp plus value.
- Never block the UI on a device. Show the last known readings immediately and update when new data lands.
- Write plain error messages.“Turn the meter on and hold it near your phone” beats any error code.
Writing back, carefully
If you write readings back to HealthKit or Google Fit, write only what the user actually recorded. Do not write derived values, estimates or anything the user did not see. That data flows into other apps, and once it is wrong there it is very hard to explain.
What App Store review will ask
Health apps get more scrutiny. Have these ready before you submit:
- Purpose strings that name the benefit.“Used to show your glucose trend to your care team”, not “This app needs health access”.
- Entitlements matching the capabilities. HealthKit has to be enabled on the identifier and in the project.
- A privacy policy that mentions health data specifically.
- No health data in analytics or crash logs. Check what your logging actually captures; a stack trace with a payload attached will fail this.
- A demo account. If the app is tied to a hospital programme, a reviewer cannot sign up. Give them working credentials and a note, or you will be rejected for something unrelated to your code.
The clinical detail engineers miss
A reading without context can be misleading. Fasting versus post-meal glucose are different clinical facts, and showing them on one line is worse than showing nothing. Keep the qualifier attached to the reading from the moment it is captured, and reflect it wherever the number appears.
Similarly, be careful with wording. An app that presents data can say “your reading is above your target range”. It should not imply a diagnosis. That is a product decision with regulatory weight, so agree it with the clinical side rather than picking copy yourself.
A checklist before you ship
- Every source normalised into one internal model, with units converted once.
- Timestamps stored in UTC, displayed in the user's local time.
- De-duplication tested with the same reading arriving twice.
- Denied-permission path tested, including the silent iOS case.
- Airplane mode and out-of-range device tested.
- No health values in logs, analytics or crash reports.
- Purpose strings, entitlements, privacy policy and demo account ready for review.
The integration is rarely the hard part. Making three disagreeing sources tell one honest story, and being careful about what that story claims, is the real work.
Read next
Flutter State Management: setState, Provider, BLoC and Riverpod compared
A practical guide to choosing state management in Flutter. What each option is good at, where each breaks down…
Flutter Performance: how to find and fix jank, slow lists and memory problems
Why Flutter apps stutter and what to do about it — rebuild scope, list building, image sizing, expensive widge…