← Back to list

❤️Integrating Apple Health and Google Health Connect in Health & Fitness Apps

Apple Health (HealthKit) on iOS and Google Health Connect on Android are the platform-native repositories for biometric, activity, and…

Rohandhalpe · 2025-12-20 19:01 · 12 claps · 7.6 min read
#apple-health-kit #health-connect #react-native #background-task #post-op-recovery
Open on Medium ↗
Wiki topics: 🌐 · Web Development 📱 · Mobile Development 💪 · Fitness & Wellness

❤️Integrating Apple Health and Google Health Connect in Health & Fitness Apps

Apple Health (HealthKit) on iOS and Google Health Connect on Android are the platform-native repositories for biometric, activity, and clinical data. Tying into them lets your app benefit from the rich, real-time insights already flowing through the OS ecosystem — steps, heart rate, sleep, vitals, and even post-op recovery metrics. Modern health apps integrate with both to avoid redundant capture screens, keep clinical teams aligned with device data, and offer context-aware guidance without forcing manual logging.

Common use cases:

  • Steps & activity: sync with streaks, rehab goals, and automated progress updates.
  • Heart rate/HRV: power recovery scoring or anomaly detection for post-op monitoring.
  • Sleep & rest: adjust coaching nudges based on the latest sleep sessions.
  • Vitals: record SpO₂, blood pressure, body temperature, glucose for clinicians or AI agents.
  • Post-op monitoring: keep surgeons and care teams informed by automatically pushing the freshest data.

Overall Architecture

  1. Device → App: The app initializes HKHealthStore or Health Connect, requests explicit read permissions, and retrieves only the metrics it renders.
  2. App → Backend: Data is normalized (units, timestamps, deduplication), serialized, and batched into a single “sync” payload to the backend — often with metadata (source ID, sample ID).
  3. Sync Strategies:
  • Background: Use HealthKit observer queries/workouts and Health Connect/WorkManager jobs; keep scopes narrow to stay within OS limits.
  • Manual: Offer a “Sync now” button for users who want immediate confirmation.
  • Periodic: Run scheduled syncs (e.g., every 6–24h) and surface “Last synced” timestamps so users trust the flow.

🔵 Apple Health Integration (iOS)

Overview

HealthKit exposes quantities, categories, and workouts through HKHealthStore. You register the data types you need, then ask the OS for permission — users approve per category. Background deliveries exist, but your app still needs to poll for new samples and aggregate them, especially when you need “latest vitals.”

Library Selection & What the App Uses

The existing app ships with react-native-health, encapsulated in lib/healthkit-service.ts. That service:

  • Handles initialization and HealthKit permissions (initHealthKit).
  • Wraps each metric fetch method (getHeartRateSamples, getDailyStepCountSamples, etc.).
  • Normalizes the result into typed TS interfaces (heart rate, blood pressure, etc.).
  • Aggregates a “latest snapshot” via getLatestHealthData.

This shared permissions set ensures you request EXACTLY the categories the app uses (heart rate, SpO₂, respiratory rate, blood pressure, body temperature, blood glucose, steps). It’s the baseline for both the initial authorization flow and any subsequent refresh.

Permissions Setup

  • Entitlements: Enable HealthKit capability.
  • Info.plist: Provide NSHealthShareUsageDescription/NSHealthUpdateUsageDescription.
  • Read vs. write: The app only reads; write is kept empty to minimize scope.
  • Review readiness: Document health-related usage in App Store metadata, focusing on recovery/monitoring use cases.

Requesting & Fetching

HealthKitService.initialize() ensures the native module is present, handles errors, and caches initialization state. After a successful init, getLatestHealthData() calls each metric fetch concurrently:

Each fetch method handles protected data errors and logs contextual debugging info, providing resilient reads even when the user temporarily locks the device.

import { NativeModules, Platform } from 'react-native';
import AppleHealthKit from 'react-native-health';
import type {
  HealthInputOptions,
  HealthKitPermissions,
  HealthValue,
  BloodPressureSampleValue,
} from 'react-native-health';

const HealthKit = NativeModules.AppleHealthKit || NativeModules.RNAppleHealthKit;

if (AppleHealthKit?.Constants && HealthKit) {
  HealthKit.Constants = AppleHealthKit.Constants;
}

/* ---------------- Permissions ---------------- */

const permissions: HealthKitPermissions = {
  permissions: {
    read: [
      HealthKit?.Constants?.Permissions?.HeartRate || 'HeartRate',
      HealthKit?.Constants?.Permissions?.OxygenSaturation || 'OxygenSaturation',
      HealthKit?.Constants?.Permissions?.RespiratoryRate || 'RespiratoryRate',
      HealthKit?.Constants?.Permissions?.BloodPressureSystolic ||
        'BloodPressureSystolic',
      HealthKit?.Constants?.Permissions?.BloodPressureDiastolic ||
        'BloodPressureDiastolic',
      HealthKit?.Constants?.Permissions?.StepCount || 'StepCount',
    ],
    write: [],
  },
};

/* ---------------- Types ---------------- */

export type BloodPressureSample = {
  systolic: number;
  diastolic: number;
  startDate: string;
  endDate: string;
};

/* ---------------- Service ---------------- */

class HealthKitService {
  private initialized = false;

  async initialize(): Promise<void> {
    if (Platform.OS !== 'ios') return;

    return new Promise((resolve, reject) => {
      HealthKit.initHealthKit(permissions, (error: string) => {
        if (error) {
          reject(new Error(error));
          return;
        }
        this.initialized = true;
        resolve();
      });
    });
  }

  private ensureInit() {
    if (!this.initialized) {
      throw new Error('HealthKit not initialized');
    }
  }

  getHeartRate(options: HealthInputOptions): Promise<HealthValue[]> {
    this.ensureInit();

    return new Promise((resolve, reject) => {
      HealthKit.getHeartRateSamples(options, (error, results) => {
        if (error) return reject(error);
        resolve(results || []);
      });
    });
  }

  getOxygenSaturation(options: HealthInputOptions): Promise<HealthValue[]> {
    this.ensureInit();

    return new Promise((resolve, reject) => {
      HealthKit.getOxygenSaturationSamples(options, (error, results) => {
        if (error) return reject(error);
        resolve(results || []);
      });
    });
  }

  getRespiratoryRate(options: HealthInputOptions): Promise<HealthValue[]> {
    this.ensureInit();

    return new Promise((resolve, reject) => {
      HealthKit.getRespiratoryRateSamples(options, (error, results) => {
        if (error) return reject(error);
        resolve(results || []);
      });
    });
  }

  getBloodPressure(
    options: HealthInputOptions,
  ): Promise<BloodPressureSample[]> {
    this.ensureInit();

    return new Promise((resolve, reject) => {
      HealthKit.getBloodPressureSamples(
        options,
        (error: string, results: BloodPressureSampleValue[]) => {
          if (error) return reject(error);

          const data =
            results?.map((s) => ({
              systolic: s.bloodPressureSystolicValue,
              diastolic: s.bloodPressureDiastolicValue,
              startDate: s.startDate,
              endDate: s.endDate,
            })) || [];

          resolve(data);
        },
      );
    });
  }

  getSteps(options: HealthInputOptions): Promise<HealthValue[]> {
    this.ensureInit();

    return new Promise((resolve, reject) => {
      HealthKit.getDailyStepCountSamples(options, (error, results) => {
        if (error) return reject(error);
        resolve(results || []);
      });
    });
  }
}

export const healthKitService = new HealthKitService();

By doing this, a dialog like the one shown in the image will appear. After granting permissions, the requested data will be received.

Parsing & Normalizing

  • Units: count() for steps, inMillimetersOfMercury for blood pressure, bpm for heart rate, kilocalorie for energy.
  • Timestamp alignment: All samples are normalized to ISO strings before syncing.
  • Deduplication: getLatestHealthData slices the latest sample per metric and logs IDs — making it easy for your backend to skip duplicates.

Sample Flow

  1. Initialize the service (healthKitService.initialize()).
  2. Request permissions using healthKitPermissions.
  3. Fetch recent data with a 24-hour window, aggregated via getLatestHealthData.
  4. Send each metric as a discrete object with value, timestamp, and optional source metadata.

🟢 Google Health Connect Integration (Android)

Overview

Google Health Connect is the Android counterpart to HealthKit. It’s a centralized store that apps connect to through the Health Connect SDK. The app keeps Health Connect optional; it checks for installation, requests scoped permissions, and reads only the metrics it needs.

The code relies on react-native-health-connect (native bridge) and runs all sync logic within hooks/use-google-health-connect-sync.ts. That hook:

  • Initializes Health Connect and verifies readRecords access.
  • Defines a metrics array (HeartRate, OxygenSaturation, BloodPressure, etc.).
  • Parses and normalizes each record before syncing to the backend.

Permissions Setup

  • Declare android.permission.health.READ_*(android.permission.health.READ_BLOOD_GLUCOSE android.permission.health.READ_BLOOD_PRESSURE . . etc) scopes, if needed in the manifest/app config.
  • Use HealthConnectClient.requestPermissions for runtime consent.
  • Provide fallbacks/rationale when permissions are denied or only partially granted.

Requesting & Normalizing Data

The hook always uses a 24-hour window (DATA_FETCH_WINDOW_MS) for queries. Each metric has a dedicated parser that attempts multiple field names to handle SDK changes or vendor differences (beatsPerMinute, bpm, heartRate, etc.). The record is then normalized into a unified entry.

import { Platform, Alert } from 'react-native';
import {
 requestPermission,
 getGrantedPermissions,
 getSdkStatus,
 openHealthConnectSettings,
} from 'react-native-health-connect';

export type HealthConnectPermission = {
 accessType: 'read' | 'write';
 recordType:
  | 'HeartRate'
  | 'OxygenSaturation'
  | 'RespiratoryRate'
  | 'BloodPressure'
  | 'BodyTemperature'
  | 'BloodGlucose'
  | 'Steps';
};

const DEFAULT_PERMISSIONS: HealthConnectPermission[] = [
 { accessType: 'read', recordType: 'HeartRate' },
 { accessType: 'read', recordType: 'OxygenSaturation' },
 { accessType: 'read', recordType: 'RespiratoryRate' },
 { accessType: 'read', recordType: 'BloodPressure' },
 { accessType: 'read', recordType: 'BodyTemperature' },
 { accessType: 'read', recordType: 'BloodGlucose' },
 { accessType: 'read', recordType: 'Steps' },
];

export async function requestHealthConnectPermissions(
 permissions: HealthConnectPermission[] = DEFAULT_PERMISSIONS,
): Promise<boolean> {
 if (Platform.OS !== 'android') {
  return false;
 }

 try {
  const sdkStatus = await getSdkStatus();
  if (sdkStatus !== 3) {
   Alert.alert(
    'Health Connect Unavailable',
    'Health Connect SDK is not available. Please ensure Health Connect is installed and updated.',
   );
   return false;
  }

  const alreadyGranted = await getGrantedPermissions();
  if (alreadyGranted && alreadyGranted.length > 0) {
   return true;
  }

  const grantedPermissions = await requestPermission(permissions);

  if (grantedPermissions && grantedPermissions.length > 0) {
   return true;
  }

  const afterGranted = await getGrantedPermissions();
  if (afterGranted && afterGranted.length > 0) {
   return true;
  }

  return new Promise((resolve) => {
   Alert.alert(
    'Grant Permissions in Health Connect',
    'Please grant permissions in Health Connect settings:\n\n' +
     '1. Tap "Open Health Connect"\n' +
     '2. Go to "App permissions"\n' +
     '3. Find your app and enable the permissions\n' +
     '4. Return to this app',
    [
     {
      text: 'Cancel',
      style: 'cancel',
      onPress: () => {
       resolve(false);
      },
     },
     {
      text: 'Open Health Connect',
      onPress: () => {
       try {
        openHealthConnectSettings();
        setTimeout(() => {
         void getGrantedPermissions().then((finalCheck) => {
          resolve(finalCheck && finalCheck.length > 0);
         });
        }, 1000);
       } catch {
        resolve(false);
       }
      },
     },
    ],
   );
  });
 } catch (error) {
  console.error('[Health Connect] Error requesting permissions:', error);
  return false;
 }
}

export async function checkHealthConnectPermissions(): Promise<boolean> {
 if (Platform.OS !== 'android') {
  return false;
 }

 try {
  const granted = await getGrantedPermissions();
  return granted && granted.length > 0;
 } catch (error) {
  console.error('[Health Connect] Error checking permissions:', error);
  return false;
 }
}

By doing this, a dialog like the one shown in the image will appear. After granting permissions, the requested data will be received.

Sync & Deduplication

  • Stored records are cached via lib/google-health-connect-storage.ts.
  • findNewGoogleHealthRecords() compares fetched metrics to stored ones by ID or value.
  • performSync() sends only new metrics and calls saveStoredGoogleHealthData()/saveLastGoogleSyncTime() on success.
  • Sync is triggered on app foreground, midday intervals, and manual toggles.

Code-to-Backend Flow

Once records are normalized, syncGoogleHealthConnectDataToBackend(recordsMap, { activityType: ‘resting’ }) handles ingestion. The hook guards against concurrent syncs with globalIsSyncing and provides hooks to invalidate/refetch observation queries post-sync.

Common Challenges & Best Practices

  • Permission denial: Surface a clear UI explaining why you need each metric and link to Settings once the system dialog is denied.
  • Cross-platform consistency: Normalize units/timestamps in a shared helper before pushing to the backend. Keep readers/writers in sync with a single schema per metric.
  • Battery: Use observer queries or WorkManager to limit polling, and cache “last synced” timestamps so you aren’t constantly reading data.
  • Privacy: Only request what you display. Use encrypted storage (Keychain, EncryptedSharedPreferences) and TLS for transit.

Security, Privacy & Compliance

  • Always log consent flows (for debugging/review) and explain them in your privacy policy.
  • Encrypt local health snapshots and metadata.
  • Document health data usage in App Store/Play Console and ensure you stay within allowed health-data categories.
  • Key takeaways: Use the shared service and hook structures shown above to keep HealthKit and Health Connect integrations DRY, resilient, and user-friendly.
  • When to integrate both: If your user base spans iOS and Android, keep the permission flows separate but pipe both sources into a single backend schema.
  • Scaling tip: Add new metrics by extending the metric lists/parsers rather than rewriting sync logic — your existing healthKitService fetchers and Health Connect parsers already cover the groundwork.

Thank you for reading my blog! If you found it helpful, feel free to share your thoughts or feedback — I’d love to hear from you.


메타데이터
post_id
f9e04218c645
slug
integrating-apple-health-and-google-health-connect-in-health-fitness-apps-f9e04218c645
url
https://medium.com/@rohandhalpe05/integrating-apple-health-and-google-health-connect-in-health-fitness-apps-f9e04218c645
canonical_url
https://medium.com/@rohandhalpe05/integrating-apple-health-and-google-health-connect-in-health-fitness-apps-f9e04218c645
author_url
https://medium.com/@rohandhalpe05
status
ok
fetched_at
2026-06-26 03:39:16