Health Connect on Android in 2026: reading and writing data without giving up local-first
A practical guide to Android's Health Connect API in 2026 — permissions, background reads, and why local-first apps should treat it as optional, not truth.
Health Connect is the on-device store Android now expects health and fitness apps to read from and write to instead of keeping their own silo. For a local-first app, it raises an honest question: is this a place to publish data, a place to read it from, or both — and does saying yes to either one quietly move your source of truth off the device you promised to keep it on. Here’s what the API actually asks of you, and the line I draw around it.
What Health Connect is, and isn’t
Health Connect is a system-level datastore, not a cloud service. Records — steps, hydration, weight, sleep sessions — live in a local database that Android mediates access to, and any app the user approves can read or write through it. That’s the appeal: a hydration app and a fitness app can share one hydration total instead of each keeping a separate, incomplete count. It’s also the risk. The moment your app writes there, any other app the user has granted read access to can see that data too. Local-first doesn’t stop meaning anything once you touch Health Connect, but it does mean something narrower: the data still never leaves the device, but it’s no longer only visible to you.
On phones without the Health Connect app pre-installed, the client library prompts an install from Play the first time you request it. Budget for that failure path — a fresh device or a stripped-down ROM can leave the API unavailable, and your permission request needs a real fallback, not a crash.
The 2026 permission model
Health permissions aren’t runtime permissions in the ActivityCompat.requestPermissions sense — they route through a dedicated rationale screen the Health Connect app owns, not your app’s own dialog. You declare intent in the manifest, then launch a contract:
val requestPermissions = registerForActivityResult(
PermissionController.createRequestPermissionResultContract()
) { granted ->
if (HealthPermission.getWritePermission(HydrationRecord::class) in granted) {
// proceed
}
}
requestPermissions.launch(
setOf(
HealthPermission.getWritePermission(HydrationRecord::class),
HealthPermission.getReadPermission(HydrationRecord::class),
)
)
The result set only tells you what was granted, never why something was denied — no “denied forever” flag, no rationale you can inspect. If a permission you expected isn’t in the returned set, the correct move is to check PermissionController.getGrantedPermissions() before every read or write and degrade gracefully, not to assume yesterday’s grant still holds. Users can revoke Health Connect permissions from system settings at any time, entirely outside your app’s lifecycle, and your next API call will simply fail as if it were never granted.
Writing and reading a record
A HydrationRecord is a value with a time range and a volume, tied to the writing app’s own metadata:
val record = HydrationRecord(
startTime = Instant.now(),
startZoneOffset = ZoneOffset.systemDefault().rules.getOffset(Instant.now()),
endTime = Instant.now(),
endZoneOffset = ZoneOffset.systemDefault().rules.getOffset(Instant.now()),
volume = Volume.milliliters(250.0),
)
healthConnectClient.insertRecords(listOf(record))
Reading back is a time-range query, not a full-table scan — you’re expected to ask for a window, not “everything”:
val response = healthConnectClient.readRecords(
ReadRecordsRequest(
recordType = HydrationRecord::class,
timeRangeFilter = TimeRangeFilter.between(
startOfToday, Instant.now()
),
)
)
Every record carries a metadata.dataOrigin, so you can tell your own writes apart from a fitness tracker’s or another app’s — useful the moment you’re aggregating a “today’s total” from more than one source and don’t want to double-count.
Background reads need a separate permission
Reading Health Connect data while your app isn’t in the foreground requires PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND on top of the per-record permission, and Android treats it as sensitive enough to warrant its own line in the rationale screen. If your use case is “sync once when the app opens,” skip it — request it only for a real background job, like a widget or a daily rollup notification that has to compute its number without the user opening the app first. Requesting it when you don’t need it doesn’t fail technically; it just makes your permission screen longer and more alarming than the feature justifies, which is the kind of thing that costs you the grant entirely.
Why I’m treating it as optional, additive data
For something like Hydrame, Health Connect is tempting for one reason: a user who also logs workouts elsewhere gets one true hydration number instead of two disagreeing ones. But it’s tempting for the wrong reason too — it’s easy to let “sync to Health Connect” quietly become “Health Connect is now the source of truth,” at which point the app’s own local database turns into a cache that can drift, and the promise that nothing leaves the device gets fuzzier every time another app is granted read access. The version I’d actually ship treats Health Connect as a one-way publish of the same local record, never a read path the app depends on to function — uninstall the Health Connect app entirely and the core experience shouldn’t notice.
The checklist
Before wiring in Health Connect: check availability and handle the missing-app case, request only the specific record permissions you use, re-check granted permissions before every access instead of caching a yes from three screens ago, scope background reads to real background work, and decide up front whether you’re publishing to Health Connect or depending on it — because the API will happily let you do either, and only one of them keeps a local-first app honest about where its data actually lives.
// Related reading
More from the journal
Biometric authentication on Android in 2026: BiometricPrompt, the Keystore, and locking a local-first app
A practical guide to androidx.biometric on Android — BiometricPrompt, CryptoObject-backed keys, device credential fallback, and the mistakes that let a fingerprint check protect nothing.
The Android In-App Review API: asking for a rating without being annoying
A practical guide to Google's In-App Review API — how it actually works, where to trigger it, and why the usual 'rate us' popup is quietly hurting your Play Store rating.
Android app shortcuts and Quick Settings Tiles: logging water without opening the app
A practical guide to Android's dynamic ShortcutManager API and TileService — how to let a one-tap action skip the app entirely, with the pitfalls that trip most implementations.