Zum Inhalt springen
Abstrakte Formen aus Mattglas: eine rhythmische Welle läuft durch ein Verbindungsstück von einem Block in einen anderen

Android Health Connect in einer Hybrid-App nutzen

Eine Hybrid-App erreicht Android Health Connect über ein Capacitor-Plugin: TypeScript beschreibt die Methoden für die Webview, Kotlin holt darunter Berechtigungen ein und liest Daten über das SDK. Dazu kommen Einträge im Manifest, eine Seite mit dem Datenschutzhinweis und die Erklärung in der Play Console.

10 min Lesezeit Performance & DevOps

Die Webview einer Hybrid-App kann Gesundheitsdaten des Telefons nicht selbst lesen. Health Connect ist eine Android-Schnittstelle, die nur nativer Code anspricht. Ein Capacitor-Plugin schließt diese Lücke: Die Weboberfläche ruft eine Methode in TypeScript auf, Capacitor reicht den Aufruf an eine Kotlin-Klasse weiter, und deren Ergebnis kommt als Promise zurück.

Für wen sich eine hybride App gegenüber PWA und nativer App eignet, klärt PWA oder native App. Hier geht es um die Umsetzung, mit dem Stand vom 18.08.2026: Capacitor 8 (Version 8.0.0 erschien am 08.12.2025, 8.5.0 am 31.07.2026) und die stabile Version 1.1.0 der Bibliothek connect-client vom 08.10.2025. Beide Code-Beispiele unten sind ein bewusst kleines, selbst geschriebenes Beispiel für Schritte und Puls, kein Ausschnitt aus einem Kundenprojekt.

Grundlagen

Was Health Connect auf dem Gerät übernimmt

Health Connect ist die Ablage für Gesundheits- und Fitnessdaten auf Android-Geräten. Apps schreiben dort zum Beispiel Schritte, Puls oder Schlafphasen hinein, andere Apps lesen sie mit Erlaubnis des Nutzers. Laut Google nimmt Health Connect alle Anfragen entgegen, speichert die Daten und verwaltet, wer lesen und schreiben darf. Der Nutzer kann jede Freigabe jederzeit zurücknehmen.

Wie Health Connect auf das Gerät kommt, hängt von der Android-Version ab:

  • Ab Android 14 (API-Level 34) ist Health Connect Teil des Betriebssystems, Google nennt das ein Framework-Modul. Eine Installation ist nicht nötig.

  • Bis Android 13 (API-Level 33) ist Health Connect eine eigene App aus dem Play Store. Sie läuft laut Google ab Android 9 (API-Level 28).

Für die App bedeutet das: Vor jedem Zugriff prüft sie mit getSdkStatus, ob Health Connect verfügbar ist. Die Methode liefert drei Werte: verfügbar, nicht verfügbar (etwa wegen zu alter Android-Version) oder „Anbieter fehlt oder braucht ein Update“. Im dritten Fall kann die App den Nutzer in den Play Store schicken. Damit die Abfrage auf älteren Geräten funktioniert, nennt das Manifest das Paket com.google.android.apps.healthdata in einem queries-Eintrag.

Eine Grenze zeigt sich erst im Betrieb: Health Connect gibt Daten standardmäßig nur bis 30 Tage vor dem Zeitpunkt der Freigabe heraus. Wer ältere Werte braucht, etwa für einen Jahresverlauf, fordert zusätzlich die Berechtigung für den Verlauf an.

Ein dickes dunkles Rohr und ein schlankes helles Rohr, ein oranges Übergangsstück verbindet beide passgenau.

Aufbau

Die Arbeitsteilung zwischen Webview und Kotlin

Das Plugin hat zwei Seiten mit klarer Aufgabe. Oben steht eine TypeScript-Schnittstelle, die festlegt, welche Methoden die Web-App aufrufen darf, mit welchen Parametern und welcher Antwort. Unten steht eine Kotlin-Klasse, die jede dieser Methoden mit dem Health-Connect-SDK umsetzt. Capacitor verbindet beide über den Namen, unter dem das Plugin auf beiden Seiten registriert ist.

Die Weboberfläche kennt dabei keine Android-Begriffe. Sie fragt „Schritte des Vortags“ ab, nicht StepsRecord und AggregateRequest. Dadurch bleibt die Web-App gleich, wenn später eine iOS-Seite mit HealthKit dazukommt; nur die native Hälfte wächst. Ob die Weboberfläche dafür als Single Page App gebaut sein muss, ist eine eigene Frage, zu der Single Page App ist nicht immer die Lösung passt.

Nach dieser Arbeitsteilung haben wir 2025 für KIWI Health ein Capacitor-Plugin gebaut, das die Hybrid-App des Kunden an Android Health Connect anschließt, mit TypeScript-Schnittstelle nach oben und nativem Code nach unten. Das Plugin ist als @kiwi-health/capacitor-health-connect im Namespace des Kunden veröffentlicht, Open Source, mit Lizenzdatei, README, generierter API-Dokumentation und einer Demo-App im example-Ordner.

TypeScript

Eine schmale Plugin-API entwerfen

Die Schnittstelle bekommt so wenige Methoden wie möglich und nur die Datentypen, die die App fachlich braucht. Jede zusätzliche Methode muss auf jeder Plattform umgesetzt, dokumentiert und gepflegt werden. Für ein Schrittziel reichen drei Methoden: Verfügbarkeit prüfen, Berechtigungen anfragen, Schritte eines Zeitraums lesen.

Die Datentypen stehen als String-Union im Code. Der TypeScript-Compiler lehnt dann einen Aufruf mit einem Typ ab, den das Plugin nicht kennt, bevor die App auf ein Gerät kommt. Die Datei definitions.ts beschreibt die Schnittstelle, index.ts registriert das Plugin mit registerPlugin aus @capacitor/core, so wie es die Capacitor-Dokumentation für Version 8 zeigt.

Eine Falle steckt im Methodennamen. Die Basisklasse Plugin von Capacitor bringt checkPermissions und requestPermissions schon mit, für die normalen Android-Laufzeitberechtigungen wie Kamera oder Standort. Wer diese Namen für Health Connect wiederverwendet, überschreibt das eingebaute Verhalten. Im Beispiel heißt die Methode deshalb requestHealthPermissions.

Das Plugin liest nur und gibt die Werte an die Weboberfläche weiter. Sollen sie zusätzlich an einen Server gehen, auch wenn das Telefon gerade kein Netz hat, gelten die Regeln aus Offline-fähige App für Außendienst.

Geprüft mit TypeScript 5.9 im strict-Modus gegen die Typen von @capacitor/core 8.5.0; ein Aufruf mit einem unbekannten Datentyp bricht die Übersetzung ab.

src/definitions.ts und src/index.ts typescript
// src/definitions.ts
export type HealthDataType = 'steps' | 'heartRate';

export type Availability = 'available' | 'updateRequired' | 'unavailable';

export interface PermissionResult {
  granted: HealthDataType[];
}

export interface HealthConnectPlugin {
  /** Maps HealthConnectClient.getSdkStatus() to three states. */
  checkAvailability(): Promise<{ availability: Availability }>;

  /** Opens the Health Connect permission screen for the given types. */
  requestHealthPermissions(options: { read: HealthDataType[] }): Promise<PermissionResult>;

  /** Sum of steps between two ISO 8601 timestamps. */
  readSteps(options: { startTime: string; endTime: string }): Promise<{ count: number }>;
}

// src/index.ts
import { registerPlugin } from '@capacitor/core';

import type { HealthConnectPlugin } from './definitions';

const HealthConnect = registerPlugin<HealthConnectPlugin>('HealthConnect');

export * from './definitions';
export { HealthConnect };

Kotlin

Berechtigungen sauber anfragen

Health Connect verlangt Berechtigungen auf drei Ebenen, und die drei müssen zueinander passen.

  1. Manifest: Jede Leseberechtigung steht als uses-permission im AndroidManifest.xml, etwa android.permission.health.READ_STEPS. Laut Google sollen sie zu den Datentypen passen, die in der Play Console angegeben sind.

  2. Datenschutzhinweis: Das Manifest braucht eine Activity, die die Datenschutzerklärung der App zeigt. Bis Android 13 reagiert sie auf die Aktion androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE, ab Android 14 kommt ein activity-alias mit VIEW_PERMISSION_USAGE und der Kategorie HEALTH_PERMISSIONS dazu. Laut Google muss dort dieselbe Datenschutzerklärung stehen wie in der Play Console. In einer Hybrid-App liegt hier ein Stück nativer Code, das mit dem eigentlichen Plugin nichts zu tun hat und deshalb als eigener Punkt in die Planung gehört.

  3. Abfrage zur Laufzeit: Die App öffnet den Freigabedialog über den Vertrag PermissionController.createRequestPermissionResultContract und prüft vor jedem Lesen mit getGrantedPermissions, ob die Freigabe noch besteht.

Für Punkt 3 hat Capacitor eine passende Stelle: startActivityForResult mit einer Methode, die mit @ActivityCallback markiert ist. Der Vertrag von Health Connect liefert über createIntent das Intent für den Dialog und wertet über parseResult die Antwort aus. Für die Rechtsgrundlage und den technischen Schutz der Daten auf dem Server gilt, was unter Gesundheitsdaten in Webanwendungen nach Art. 9 DSGVO steht.

Ein Detail im Lesecode betrifft die Zeitangaben: Die Web-App schickt Zeitpunkte mit Zeitzone, etwa 2026-08-17T00:00:00+02:00. Der Kotlin-Code wandelt sie über OffsetDateTime in ein Instant um, das Offsets und „Z“ gleichermaßen versteht. Eine ungültige Zeitangabe oder ein Start nach dem Ende lehnt die Methode mit einer eigenen Meldung ab, bevor sie Health Connect anfragt, und vor dem Freigabedialog prüft das Plugin, ob Health Connect auf dem Gerät verfügbar ist. Schritte werden über aggregate summiert und nicht über readRecords gelesen, weil Google für kumulative Typen wie Schritte die Aggregation empfiehlt, um doppelte Zählung aus mehreren Quellen zu vermeiden.

Nicht übersetzt und nicht auf einem Gerät ausgeführt: Wir haben jede Zeile mit der Health-Connect-Dokumentation von Google und den Capacitor-Quellen der Version 8.5.0 abgeglichen.

android/src/main/java/com/example/healthconnect/HealthConnectPlugin.kt kotlin
package com.example.healthconnect

import androidx.activity.result.ActivityResult
import androidx.health.connect.client.HealthConnectClient
import androidx.health.connect.client.PermissionController
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.StepsRecord
import androidx.health.connect.client.request.AggregateRequest
import androidx.health.connect.client.time.TimeRangeFilter
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.ActivityCallback
import com.getcapacitor.annotation.CapacitorPlugin
import java.time.OffsetDateTime
import java.time.format.DateTimeParseException
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch

@CapacitorPlugin(name = "HealthConnect")
class HealthConnectPlugin : Plugin() {

    private val scope = MainScope()
    private val permissionContract = PermissionController.createRequestPermissionResultContract()

    // Web-side type name -> Health Connect read permission
    private val readPermissions = mapOf(
        "steps" to HealthPermission.getReadPermission(StepsRecord::class),
        "heartRate" to HealthPermission.getReadPermission(HeartRateRecord::class),
    )

    @PluginMethod
    fun checkAvailability(call: PluginCall) {
        val availability = when (HealthConnectClient.getSdkStatus(context)) {
            HealthConnectClient.SDK_AVAILABLE -> "available"
            HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED -> "updateRequired"
            else -> "unavailable"
        }
        val ret = JSObject()
        ret.put("availability", availability)
        call.resolve(ret)
    }

    @PluginMethod
    fun requestHealthPermissions(call: PluginCall) {
        if (HealthConnectClient.getSdkStatus(context) != HealthConnectClient.SDK_AVAILABLE) {
            call.reject("Health Connect is not available on this device")
            return
        }
        val types = call.getArray("read")?.toList<String>() ?: emptyList()
        val unknown = types.filterNot { it in readPermissions }
        if (types.isEmpty() || unknown.isNotEmpty()) {
            call.reject("Unknown or missing data types: $unknown")
            return
        }
        val wanted = types.map { readPermissions.getValue(it) }.toSet()
        val intent = permissionContract.createIntent(context, wanted)
        startActivityForResult(call, intent, "onPermissionResult")
    }

    @ActivityCallback
    private fun onPermissionResult(call: PluginCall, result: ActivityResult) {
        val granted = permissionContract.parseResult(result.resultCode, result.data)
        val types = JSArray()
        readPermissions.filterValues { it in granted }.keys.forEach { types.put(it) }
        val ret = JSObject()
        ret.put("granted", types)
        call.resolve(ret)
    }

    @PluginMethod
    fun readSteps(call: PluginCall) {
        val start = call.getString("startTime")
        val end = call.getString("endTime")
        if (start == null || end == null) {
            call.reject("startTime and endTime are required")
            return
        }
        val (startInstant, endInstant) = try {
            OffsetDateTime.parse(start).toInstant() to OffsetDateTime.parse(end).toInstant()
        } catch (e: DateTimeParseException) {
            call.reject("startTime and endTime must be ISO 8601 timestamps with an offset", e)
            return
        }
        if (!startInstant.isBefore(endInstant)) {
            call.reject("startTime must be before endTime")
            return
        }
        scope.launch {
            try {
                val client = HealthConnectClient.getOrCreate(context)
                val granted = client.permissionController.getGrantedPermissions()
                if (readPermissions.getValue("steps") !in granted) {
                    call.reject("Permission for steps not granted")
                    return@launch
                }
                val response = client.aggregate(
                    AggregateRequest(
                        metrics = setOf(StepsRecord.COUNT_TOTAL),
                        timeRangeFilter = TimeRangeFilter.between(startInstant, endInstant),
                    )
                )
                val ret = JSObject()
                ret.put("count", response[StepsRecord.COUNT_TOTAL] ?: 0L)
                call.resolve(ret)
            } catch (e: Exception) {
                call.reject(e.message ?: "Reading steps failed", e)
            }
        }
    }

    override fun handleOnDestroy() {
        scope.cancel()
    }
}

Auslieferung

Testen und veröffentlichen

Getestet wird auf mindestens zwei Geräten oder Emulatoren: einem mit Android 14 oder neuer, wo Health Connect zum System gehört, und einem mit Android 13 oder älter, wo die Health-Connect-App aus dem Play Store installiert sein muss. Auf beiden lohnen die Randfälle einen eigenen Durchlauf: Freigabe verweigert, Freigabe später in den Einstellungen entzogen, Health Connect deinstalliert oder veraltet. Für automatische Tests bietet Google die Bibliothek connect-testing mit einem FakeHealthConnectClient an, am 18.08.2026 in der Alpha-Version 1.0.0-alpha04 vom 12.08.2026.

Vor der Veröffentlichung verlangt Google drei Schritte in der Play Console:

  • Richtlinien: Die App hält die Regeln zu Nutzerdaten und zu Berechtigungen für sensible Daten ein, mit den Zusatzanforderungen für Health Connect.

  • Datensicherheit: Der Abschnitt zur Datensicherheit nennt, welche Gesundheitsdaten die App erhebt und ob sie sie teilt.

  • Erklärung zu Gesundheits-Apps: Unter Richtlinie und App-Inhalte gibt die App ihre Gesundheitsfunktionen an und begründet jeden Health-Connect-Datentyp einzeln. Google verlangt, nur die Typen anzufragen, die eine für Nutzer sichtbare Funktion braucht.

Kommt später ein Datentyp hinzu, wird die Erklärung neu eingereicht, mit allen weiterhin genutzten Typen. Eine neue App-Version allein braucht keinen neuen Antrag, die Freigabe hängt am Paketnamen; Google kann die Erklärung bei einer neuen Version aber erneut prüfen. Fehlt die Freigabe, sehen Nutzer beim Verbinden mit Health Connect einen Hinweis, dass die App nicht zugreifen kann. Die Zeit für die Prüfung gehört deshalb in den Releaseplan, bevor ein Termin für den Start zugesagt wird. Wie Planung, Entwicklung und Store-Veröffentlichung einer App zusammenhängen, beschreibt unsere Seite zur App-Entwicklung.

Health Connect

Häufige Fragen

  • Sie brauchen nativen Android-Code, aber keine komplett native App. Eine Hybrid-App mit Capacitor behält ihre Weboberfläche und bekommt ein Plugin, dessen Android-Teil in Kotlin oder Java mit dem Health-Connect-SDK spricht. Eine reine PWA im Browser erreicht Health Connect nicht, weil Google Health Connect nur als Android-Schnittstelle anbietet.

  • Die Datentypen haben jeweils eigene Lese- und Schreibberechtigungen, etwa android.permission.health.READ_STEPS für Schritte. Sie werden im Manifest erklärt, in der Play Console begründet und zur Laufzeit über den Freigabedialog von Health Connect eingeholt. Für Daten, die älter als 30 Tage vor der Freigabe sind, und für das Lesen im Hintergrund gibt es zusätzliche Berechtigungen.

Ihre Web-App soll an Gesundheitsdaten des Telefons?

Wenn Sie wissen möchten, ob sich Ihre bestehende Weboberfläche mit einem Plugin für Health Connect erweitern lässt, besprechen wir das im kostenfreien Erstgespräch von 30 Minuten, mit Blick auf Berechtigungen, Play-Erklärung und Aufwand.

App-Entwicklung bei MARCODES