WellAlly Logo
WellAlly康心伴
Development

Integrating Apple HealthKit and Google Fit with React Native Expo

Learn how to integrate Apple HealthKit and Google Fit into a React Native Expo app to read and write health data such as steps, heart rate, sleep, and workouts. This guide covers permission handling, data sync, and cross-platform abstraction.

W
WellAlly Dev Team
2026-04-06
12 min read

What You'll Build

You will build a cross-platform health data integration layer that reads step counts, heart rate readings, sleep analysis, and workout data from Apple HealthKit on iOS and Google Fit on Android. The module provides a unified TypeScript API so your app code does not need platform-specific branching. You will also implement background sync, permission management, and data caching.

This is the same integration pattern used in WellAlly to pull patient activity and vitals data into the unified health dashboard.

Prerequisites

  • React Native project using Expo SDK 52+ with development builds
  • Apple Developer account (for HealthKit capability)
  • Google Cloud Console project with Fitness API enabled
  • Physical iOS device (HealthKit does not work in Simulator)
  • Physical Android device or emulator with Google Play Services
  • Expo Application Services (EAS) CLI installed

Tech Stack

TechnologyVersionPurpose
React Native0.76.x (Expo)Mobile framework
expo-health-kit1.xiOS HealthKit access
@ovalmoney/react-native-health-kit3.xAlternative HealthKit bridge
@kingstinct/react-native-healthkit7.xFull HealthKit types and queries
expo-dev-client5.xDevelopment builds for native modules
TypeScript5.xType safety
react-native-mmkv3.xFast local cache

Architecture Overview

The integration uses a platform abstraction layer. A unified HealthDataService interface is implemented separately for iOS and Android. The factory pattern selects the correct implementation at runtime.

code
HealthDataProvider (React Context)
  ├── useHealthData()                -- unified hook
  ├── HealthDataService (interface)
  │     ├── iOSHealthKitService      -- wraps HealthKit
  │     └── AndroidGoogleFitService   -- wraps Google Fit
  ├── PermissionManager
  │     ├── requestPermissions()
  │     └── checkPermissionStatus()
  ├── DataSyncEngine
  │     ├── fetchDailySummary()
  │     ├── fetchTimeSeries()
  │     └── writeWorkout()
  └── LocalCache (MMKV)
        ├── getCachedData()
        └── setCachedData()
Code collapsed

Step-by-Step Implementation

Step 1: Expo Development Build Setup

HealthKit and Google Fit require native modules. You need a development build rather than Expo Go.

code
# Install EAS CLI if not already installed
npm install -g eas-cli

# Configure the project
eas build:configure

# Install required packages
npx expo install expo-dev-client expo-build-properties
npm install @kingstinct/react-native-healthkit
npm install react-native-google-fit
npm install react-native-mmkv
Code collapsed

Add HealthKit capability to your iOS configuration.

code
// app.json
{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSHealthShareUsageDescription": "WellAlly reads your health data to provide personalized health insights and track your wellness journey.",
        "NSHealthUpdateUsageDescription": "WellAlly writes workout and health data to help you maintain a complete health record."
      }
    },
    "plugins": [
      [
        "expo-build-properties",
        {
          "ios": {
            "entitlements": {
              "health-kit": true
            }
          }
        }
      ]
    ]
  }
}
Code collapsed

Step 2: Define the Unified Health Data Types

Create shared types that abstract away platform-specific data formats.

code
// src/types/health-data.ts

export interface HealthPermission {
  read: HealthDataType[];
  write: HealthDataType[];
}

export type HealthDataType =
  | 'steps'
  | 'heartRate'
  | 'sleepAnalysis'
  | 'activeEnergyBurned'
  | 'distanceWalkingRunning'
  | 'workout'
  | 'bloodGlucose'
  | 'bloodPressureSystolic'
  | 'bloodPressureDiastolic'
  | 'bodyMass'
  | 'oxygenSaturation';

export interface HealthDataPoint {
  value: number;
  unit: string;
  startDate: string;
  endDate: string;
  source: 'apple_health' | 'google_fit' | 'manual';
  metadata?: Record<string, unknown>;
}

export interface DailyHealthSummary {
  date: string;
  steps: number;
  heartRate: { avg: number; min: number; max: number };
  activeCalories: number;
  sleepHours: number;
  distanceKm: number;
}

export interface WorkoutEntry {
  type: WorkoutType;
  startDate: string;
  endDate: string;
  caloriesBurned: number;
  distanceKm?: number;
  avgHeartRate?: number;
  source: string;
}

export type WorkoutType =
  | 'running'
  | 'walking'
  | 'cycling'
  | 'swimming'
  | 'strength'
  | 'yoga'
  | 'hiit'
  | 'other';

export interface HealthDataQuery {
  dataType: HealthDataType;
  startDate: Date;
  endDate: Date;
  limit?: number;
  ascending?: boolean;
}

export interface PermissionStatus {
  dataType: HealthDataType;
  read: 'granted' | 'denied' | 'notDetermined';
  write: 'granted' | 'denied' | 'notDetermined';
}
Code collapsed

Step 3: Define the Service Interface

code
// src/services/HealthDataService.ts

import type {
  HealthDataPoint,
  DailyHealthSummary,
  WorkoutEntry,
  HealthDataQuery,
  PermissionStatus,
  HealthDataType,
} from '../types/health-data';

export interface HealthDataService {
  isAvailable(): Promise<boolean>;

  requestPermissions(readTypes: HealthDataType[], writeTypes: HealthDataType[]): Promise<boolean>;

  getPermissionStatus(types: HealthDataType[]): Promise<PermissionStatus[]>;

  queryData(query: HealthDataQuery): Promise<HealthDataPoint[]>;

  getDailySummary(date: Date): Promise<DailyHealthSummary>;

  getDailySummaries(startDate: Date, endDate: Date): Promise<DailyHealthSummary[]>;

  writeWorkout(workout: WorkoutEntry): Promise<boolean>;

  writeDataPoint(dataType: HealthDataType, value: number, startDate: Date, endDate: Date): Promise<boolean>;
}
Code collapsed

Step 4: iOS HealthKit Implementation

code
// src/services/IOSHealthKitService.ts

import HealthKit, {
  HKQuantityTypeIdentifier,
  HKCategoryTypeIdentifier,
  HKWorkoutType,
  HKUnit,
  HKSampleSortIdentifier,
} from '@kingstinct/react-native-healthkit';
import type { HealthDataService } from './HealthDataService';
import type {
  HealthDataPoint,
  DailyHealthSummary,
  WorkoutEntry,
  HealthDataQuery,
  PermissionStatus,
  HealthDataType,
} from '../types/health-data';

const dataTypeToHKIdentifier: Record<HealthDataType, string> = {
  steps: HKQuantityTypeIdentifier.stepCount,
  heartRate: HKQuantityTypeIdentifier.heartRate,
  activeEnergyBurned: HKQuantityTypeIdentifier.activeEnergyBurned,
  distanceWalkingRunning: HKQuantityTypeIdentifier.distanceWalkingRunning,
  bloodGlucose: HKQuantityTypeIdentifier.bloodGlucose,
  bloodPressureSystolic: HKQuantityTypeIdentifier.bloodPressureSystolic,
  bloodPressureDiastolic: HKQuantityTypeIdentifier.bloodPressureDiastolic,
  bodyMass: HKQuantityTypeIdentifier.bodyMass,
  oxygenSaturation: HKQuantityTypeIdentifier.oxygenSaturation,
  sleepAnalysis: HKCategoryTypeIdentifier.sleepAnalysis,
  workout: HKWorkoutType,
};

const dataTypeToHKUnit: Record<HealthDataType, string> = {
  steps: HKUnit.count,
  heartRate: 'count/min',
  activeEnergyBurned: HKUnit.kilocalorie,
  distanceWalkingRunning: HKUnit.meter,
  bloodGlucose: 'mmol/L',
  bloodPressureSystolic: 'mmHg',
  bloodPressureDiastolic: 'mmHg',
  bodyMass: HKUnit.gram,
  oxygenSaturation: 'percent',
  sleepAnalysis: HKUnit.minute,
  workout: HKUnit.count,
};

export class IOSHealthKitService implements HealthDataService {
  async isAvailable(): Promise<boolean> {
    return HealthKit.isAvailable();
  }

  async requestPermissions(readTypes: HealthDataType[], writeTypes: HealthDataType[]): Promise<boolean> {
    try {
      const readIdentifiers = readTypes
        .map((t) => dataTypeToHKIdentifier[t])
        .filter(Boolean);
      const writeIdentifiers = writeTypes
        .map((t) => dataTypeToHKIdentifier[t])
        .filter(Boolean);

      await HealthKit.requestPermissions(readIdentifiers, writeIdentifiers);
      return true;
    } catch {
      return false;
    }
  }

  async getPermissionStatus(types: HealthDataType[]): Promise<PermissionStatus[]> {
    // HealthKit does not expose per-type permission status directly.
    // Attempt a zero-range query to detect authorization.
    const statuses: PermissionStatus[] = [];
    for (const dataType of types) {
      const hkIdentifier = dataTypeToHKIdentifier[dataType];
      const status: PermissionStatus = {
        dataType,
        read: 'notDetermined',
        write: 'notDetermined',
      };

      try {
        const now = new Date();
        const results = await HealthKit.queryQuantitySamples(hkIdentifier, {
          from: now,
          to: now,
          limit: 1,
        });
        status.read = results.length >= 0 ? 'granted' : 'denied';
      } catch {
        status.read = 'denied';
      }

      statuses.push(status);
    }
    return statuses;
  }

  async queryData(query: HealthDataQuery): Promise<HealthDataPoint[]> {
    const hkIdentifier = dataTypeToHKIdentifier[query.dataType];
    if (!hkIdentifier) return [];

    try {
      const samples = await HealthKit.queryQuantitySamples(hkIdentifier, {
        from: query.startDate,
        to: query.endDate,
        limit: query.limit ?? 1000,
        sort: query.ascending !== false
          ? HKSampleSortIdentifier.startDate
          : undefined,
      });

      return samples.map((sample) => ({
        value: sample.quantity,
        unit: dataTypeToHKUnit[query.dataType],
        startDate: sample.startDate,
        endDate: sample.endDate,
        source: 'apple_health' as const,
        metadata: sample.metadata as Record<string, unknown> | undefined,
      }));
    } catch {
      return [];
    }
  }

  async getDailySummary(date: Date): Promise<DailyHealthSummary> {
    const startOfDay = new Date(date);
    startOfDay.setHours(0, 0, 0, 0);
    const endOfDay = new Date(date);
    endOfDay.setHours(23, 59, 59, 999);

    const [stepsData, heartRateData, caloriesData, distanceData, sleepData] =
      await Promise.all([
        this.queryData({ dataType: 'steps', startDate: startOfDay, endDate: endOfDay }),
        this.queryData({ dataType: 'heartRate', startDate: startOfDay, endDate: endOfDay }),
        this.queryData({ dataType: 'activeEnergyBurned', startDate: startOfDay, endDate: endOfDay }),
        this.queryData({ dataType: 'distanceWalkingRunning', startDate: startOfDay, endDate: endOfDay }),
        this.queryData({ dataType: 'sleepAnalysis', startDate: startOfDay, endDate: endOfDay }),
      ]);

    const totalSteps = stepsData.reduce((sum, d) => sum + d.value, 0);
    const hrValues = heartRateData.map((d) => d.value);
    const totalCalories = caloriesData.reduce((sum, d) => sum + d.value, 0);
    const totalDistance = distanceData.reduce((sum, d) => sum + d.value, 0) / 1000;
    const totalSleepMinutes = sleepData.reduce((sum, d) => sum + d.value, 0);

    return {
      date: startOfDay.toISOString().split('T')[0],
      steps: Math.round(totalSteps),
      heartRate: {
        avg: hrValues.length ? Math.round(hrValues.reduce((a, b) => a + b, 0) / hrValues.length) : 0,
        min: hrValues.length ? Math.round(Math.min(...hrValues)) : 0,
        max: hrValues.length ? Math.round(Math.max(...hrValues)) : 0,
      },
      activeCalories: Math.round(totalCalories),
      sleepHours: Math.round((totalSleepMinutes / 60) * 10) / 10,
      distanceKm: Math.round(totalDistance * 100) / 100,
    };
  }

  async getDailySummaries(startDate: Date, endDate: Date): Promise<DailyHealthSummary[]> {
    const summaries: DailyHealthSummary[] = [];
    const current = new Date(startDate);

    while (current <= endDate) {
      const summary = await this.getDailySummary(new Date(current));
      summaries.push(summary);
      current.setDate(current.getDate() + 1);
    }

    return summaries;
  }

  async writeWorkout(workout: WorkoutEntry): Promise<boolean> {
    try {
      await HealthKit.saveWorkout({
        type: this.mapWorkoutType(workout.type),
        startDate: workout.startDate,
        endDate: workout.endDate,
        totalEnergyBurned: workout.caloriesBurned,
        totalEnergyBurnedUnit: HKUnit.kilocalorie,
        totalDistance: workout.distanceKm,
        totalDistanceUnit: HKUnit.kilometer,
      });
      return true;
    } catch {
      return false;
    }
  }

  async writeDataPoint(
    dataType: HealthDataType,
    value: number,
    startDate: Date,
    endDate: Date
  ): Promise<boolean> {
    const hkIdentifier = dataTypeToHKIdentifier[dataType];
    if (!hkIdentifier) return false;

    try {
      await HealthKit.saveQuantitySample(hkIdentifier, HKUnit.count, value, {
        start: startDate,
        end: endDate,
      });
      return true;
    } catch {
      return false;
    }
  }

  private mapWorkoutType(type: string): string {
    const mapping: Record<string, string> = {
      running: 'running',
      walking: 'walking',
      cycling: 'cycling',
      swimming: 'swimming',
      strength: 'traditionalStrengthTraining',
      yoga: 'yoga',
      hiit: 'highIntensityIntervalTraining',
      other: 'other',
    };
    return mapping[type] ?? 'other';
  }
}
Code collapsed

Step 5: Android Google Fit Implementation

code
// src/services/AndroidGoogleFitService.ts

import GoogleFit from 'react-native-google-fit';
import type { HealthDataService } from './HealthDataService';
import type {
  HealthDataPoint,
  DailyHealthSummary,
  WorkoutEntry,
  HealthDataQuery,
  PermissionStatus,
  HealthDataType,
} from '../types/health-data';

export class AndroidGoogleFitService implements HealthDataService {
  async isAvailable(): Promise<boolean> {
    try {
      return await GoogleFit.isAvailable();
    } catch {
      return false;
    }
  }

  async requestPermissions(readTypes: HealthDataType[], writeTypes: HealthDataType[]): Promise<boolean> {
    try {
      const scopes = this.mapTypesToScopes(readTypes);
      const result = await GoogleFit.authorize({ scopes });
      return result.success;
    } catch {
      return false;
    }
  }

  async getPermissionStatus(types: HealthDataType[]): Promise<PermissionStatus[]> {
    // Google Fit does not expose per-scope permission status.
    // Return notDetermined and rely on the query fallback.
    return types.map((dataType) => ({
      dataType,
      read: 'notDetermined' as const,
      write: 'notDetermined' as const,
    }));
  }

  async queryData(query: HealthDataQuery): Promise<HealthDataPoint[]> {
    const options = {
      startDate: query.startDate.toISOString(),
      endDate: query.endDate.toISOString(),
    };

    try {
      switch (query.dataType) {
        case 'steps': {
          const result = await GoogleFit.getDailyStepCountSamples(options);
          return result.flatMap((day: any) =>
            (day.steps ?? []).map((s: any) => ({
              value: s.value ?? s.steps ?? 0,
              unit: 'count',
              startDate: s.startDate ?? day.startDate,
              endDate: s.endDate ?? day.endDate,
              source: 'google_fit' as const,
            }))
          );
        }
        case 'heartRate': {
          const result = await GoogleFit.getHeartRateSamples(options);
          return result.map((s: any) => ({
            value: s.value,
            unit: 'bpm',
            startDate: s.startDate,
            endDate: s.endDate,
            source: 'google_fit' as const,
          }));
        }
        case 'distanceWalkingRunning': {
          const result = await GoogleFit.getDailyDistanceSamples(options);
          return result.map((s: any) => ({
            value: s.distance,
            unit: 'meter',
            startDate: s.startDate,
            endDate: s.endDate,
            source: 'google_fit' as const,
          }));
        }
        case 'activeEnergyBurned': {
          const result = await GoogleFit.getDailyCalorieSamples(options);
          return result.map((s: any) => ({
            value: s.calorie,
            unit: 'kcal',
            startDate: s.startDate,
            endDate: s.endDate,
            source: 'google_fit' as const,
          }));
        }
        default:
          return [];
      }
    } catch {
      return [];
    }
  }

  async getDailySummary(date: Date): Promise<DailyHealthSummary> {
    const startOfDay = new Date(date);
    startOfDay.setHours(0, 0, 0, 0);
    const endOfDay = new Date(date);
    endOfDay.setHours(23, 59, 59, 999);

    const [stepsData, heartRateData, caloriesData, distanceData] = await Promise.all([
      this.queryData({ dataType: 'steps', startDate: startOfDay, endDate: endOfDay }),
      this.queryData({ dataType: 'heartRate', startDate: startOfDay, endDate: endOfDay }),
      this.queryData({ dataType: 'activeEnergyBurned', startDate: startOfDay, endDate: endOfDay }),
      this.queryData({ dataType: 'distanceWalkingRunning', startDate: startOfDay, endDate: endOfDay }),
    ]);

    const totalSteps = stepsData.reduce((sum, d) => sum + d.value, 0);
    const hrValues = heartRateData.map((d) => d.value);
    const totalCalories = caloriesData.reduce((sum, d) => sum + d.value, 0);
    const totalDistance = distanceData.reduce((sum, d) => sum + d.value, 0) / 1000;

    return {
      date: startOfDay.toISOString().split('T')[0],
      steps: Math.round(totalSteps),
      heartRate: {
        avg: hrValues.length ? Math.round(hrValues.reduce((a, b) => a + b, 0) / hrValues.length) : 0,
        min: hrValues.length ? Math.round(Math.min(...hrValues)) : 0,
        max: hrValues.length ? Math.round(Math.max(...hrValues)) : 0,
      },
      activeCalories: Math.round(totalCalories),
      sleepHours: 0, // Google Fit sleep requires separate Fitness History API
      distanceKm: Math.round(totalDistance * 100) / 100,
    };
  }

  async getDailySummaries(startDate: Date, endDate: Date): Promise<DailyHealthSummary[]> {
    const summaries: DailyHealthSummary[] = [];
    const current = new Date(startDate);
    while (current <= endDate) {
      summaries.push(await this.getDailySummary(new Date(current)));
      current.setDate(current.getDate() + 1);
    }
    return summaries;
  }

  async writeWorkout(workout: WorkoutEntry): Promise<boolean> {
    try {
      await GoogleFit.saveWorkout({
        startDate: new Date(workout.startDate).toISOString(),
        endDate: new Date(workout.endDate).toISOString(),
        sessionName: `WellAlly ${workout.type}`,
        description: workout.type,
        activityType: this.mapWorkoutType(workout.type),
        calories: workout.caloriesBurned,
      });
      return true;
    } catch {
      return false;
    }
  }

  async writeDataPoint(): Promise<boolean> {
    // Google Fit requires specific insert APIs per data type.
    // Implement as needed for your write use cases.
    return false;
  }

  private mapTypesToScopes(types: HealthDataType[]): any[] {
    const scopeMap: Partial<Record<HealthDataType, any>> = {
      steps: { access: 'read', type: 'step' },
      heartRate: { access: 'read', type: 'heartRate' },
      activeEnergyBurned: { access: 'read', type: 'calories' },
      distanceWalkingRunning: { access: 'read', type: 'distance' },
    };
    return types.map((t) => scopeMap[t]).filter(Boolean);
  }

  private mapWorkoutType(type: string): any {
    const mapping: Record<string, number> = {
      running: 8, // ActivityType.RUNNING
      walking: 7,
      cycling: 1,
      swimming: 82,
      strength: 36,
      other: 0,
    };
    return mapping[type] ?? 0;
  }
}
Code collapsed

Step 6: Platform Factory and React Context

code
// src/services/HealthServiceFactory.ts

import { Platform } from 'react-native';
import type { HealthDataService } from './HealthDataService';
import { IOSHealthKitService } from './IOSHealthKitService';
import { AndroidGoogleFitService } from './AndroidGoogleFitService';

export function createHealthService(): HealthDataService {
  if (Platform.OS === 'ios') {
    return new IOSHealthKitService();
  }
  return new AndroidGoogleFitService();
}
Code collapsed
code
// src/contexts/HealthDataContext.tsx

import React, { createContext, useContext, useEffect, useState, useRef } from 'react';
import { createHealthService } from '../services/HealthServiceFactory';
import type { HealthDataService, DailyHealthSummary, PermissionStatus, HealthDataType } from '../types/health-data';
import { MMKV } from 'react-native-mmkv';

const cache = new MMKV({ id: 'health-data-cache' });

interface HealthDataContextValue {
  service: HealthDataService;
  isAvailable: boolean;
  permissions: PermissionStatus[];
  requestPermissions: (read: HealthDataType[], write: HealthDataType[]) => Promise<boolean>;
  getDailySummary: (date: Date) => Promise<DailyHealthSummary>;
  loading: boolean;
}

const HealthDataContext = createContext<HealthDataContextValue | null>(null);

export function HealthDataProvider({ children }: { children: React.ReactNode }) {
  const serviceRef = useRef<HealthDataService>(createHealthService());
  const [isAvailable, setIsAvailable] = useState(false);
  const [permissions, setPermissions] = useState<PermissionStatus[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function checkAvailability() {
      const available = await serviceRef.current.isAvailable();
      setIsAvailable(available);
      setLoading(false);
    }
    checkAvailability();
  }, []);

  const requestPermissions = async (read: HealthDataType[], write: HealthDataType[]) => {
    const granted = await serviceRef.current.requestPermissions(read, write);
    if (granted) {
      const status = await serviceRef.current.getPermissionStatus(read);
      setPermissions(status);
    }
    return granted;
  };

  const getDailySummary = async (date: Date): Promise<DailyHealthSummary> => {
    const cacheKey = `summary-${date.toISOString().split('T')[0]}`;
    const cached = cache.getString(cacheKey);
    if (cached) {
      return JSON.parse(cached);
    }

    const summary = await serviceRef.current.getDailySummary(date);
    cache.set(cacheKey, JSON.stringify(summary));

    // Cache expires after 15 minutes
    setTimeout(() => cache.delete(cacheKey), 15 * 60 * 1000);

    return summary;
  };

  return (
    <HealthDataContext.Provider
      value={{
        service: serviceRef.current,
        isAvailable,
        permissions,
        requestPermissions,
        getDailySummary,
        loading,
      }}
    >
      {children}
    </HealthDataContext.Provider>
  );
}

export function useHealthData() {
  const context = useContext(HealthDataContext);
  if (!context) {
    throw new Error('useHealthData must be used within a HealthDataProvider');
  }
  return context;
}
Code collapsed

Step 7: Using the Hook in a Screen

code
// src/screens/HealthDashboardScreen.tsx

import React, { useEffect, useState } from 'react';
import { View, Text, ScrollView, TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native';
import { useHealthData } from '../contexts/HealthDataContext';
import type { DailyHealthSummary } from '../types/health-data';

const REQUIRED_READ_TYPES = ['steps', 'heartRate', 'activeEnergyBurned', 'distanceWalkingRunning'] as const;
const REQUIRED_WRITE_TYPES = ['workout'] as const;

export function HealthDashboardScreen() {
  const { isAvailable, requestPermissions, getDailySummary, loading } = useHealthData();
  const [summary, setSummary] = useState<DailyHealthSummary | null>(null);
  const [hasPermissions, setHasPermissions] = useState(false);

  useEffect(() => {
    if (hasPermissions) {
      getDailySummary(new Date()).then(setSummary);
    }
  }, [hasPermissions]);

  const handleRequestPermissions = async () => {
    const granted = await requestPermissions(
      REQUIRED_READ_TYPES as unknown as any[],
      REQUIRED_WRITE_TYPES as unknown as any[]
    );
    setHasPermissions(granted);
  };

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#3b82f6" />
      </View>
    );
  }

  if (!isAvailable) {
    return (
      <View style={styles.center}>
        <Text style={styles.unavailableText}>
          Health data is not available on this device.
        </Text>
      </View>
    );
  }

  if (!hasPermissions) {
    return (
      <View style={styles.center}>
        <Text style={styles.permissionTitle}>Connect Your Health Data</Text>
        <Text style={styles.permissionSubtext}>
          WellAlly needs access to read your health data to provide personalized insights.
        </Text>
        <TouchableOpacity style={styles.permissionButton} onPress={handleRequestPermissions}>
          <Text style={styles.permissionButtonText}>Grant Permissions</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.header}>Today's Health Summary</Text>

      {summary && (
        <View style={styles.cardGrid}>
          <MetricCard label="Steps" value={summary.steps.toLocaleString()} unit="steps" />
          <MetricCard label="Avg Heart Rate" value={`${summary.heartRate.avg}`} unit="bpm" />
          <MetricCard label="Calories" value={`${summary.activeCalories}`} unit="kcal" />
          <MetricCard label="Distance" value={`${summary.distanceKm}`} unit="km" />
          <MetricCard label="Sleep" value={`${summary.sleepHours}`} unit="hours" />
          <MetricCard
            label="Heart Rate Range"
            value={`${summary.heartRate.min}-${summary.heartRate.max}`}
            unit="bpm"
          />
        </View>
      )}
    </ScrollView>
  );
}

function MetricCard({ label, value, unit }: { label: string; value: string; unit: string }) {
  return (
    <View style={styles.metricCard}>
      <Text style={styles.metricLabel}>{label}</Text>
      <Text style={styles.metricValue}>
        {value} <Text style={styles.metricUnit}>{unit}</Text>
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#f8fafc', padding: 16 },
  center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
  header: { fontSize: 22, fontWeight: '700', color: '#0f172a', marginBottom: 16 },
  cardGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12 },
  metricCard: {
    flex: 1,
    minWidth: '45%',
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    shadowColor: '#000',
    shadowOpacity: 0.05,
    shadowRadius: 8,
    elevation: 2,
  },
  metricLabel: { fontSize: 13, color: '#64748b', marginBottom: 4 },
  metricValue: { fontSize: 24, fontWeight: '700', color: '#0f172a' },
  metricUnit: { fontSize: 13, fontWeight: '400', color: '#94a3b8' },
  unavailableText: { fontSize: 16, color: '#64748b', textAlign: 'center' },
  permissionTitle: { fontSize: 20, fontWeight: '700', color: '#0f172a', marginBottom: 8 },
  permissionSubtext: { fontSize: 14, color: '#64748b', textAlign: 'center', marginBottom: 24 },
  permissionButton: { backgroundColor: '#3b82f6', paddingHorizontal: 24, paddingVertical: 14, borderRadius: 10 },
  permissionButtonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
});
Code collapsed

Best Practices

Permission Handling

  • Request permissions in context: Only ask for HealthKit or Google Fit access when the user navigates to a feature that needs it. Do not request all permissions on first launch.
  • Explain why you need the data: Show a pre-permission screen explaining what data you read and why before triggering the system permission dialog.
  • Handle denial gracefully: If the user denies permission, show them what they are missing and provide a path to Settings to re-enable.

Data Sync

  • Use incremental sync: Track the last sync timestamp and only query data from that point forward instead of pulling the entire history every time.
  • Cache aggressively with MMKV: Health data does not change for past dates. Cache historical summaries permanently and only refresh today's data.
  • Batch API calls: Use Promise.all for independent queries to minimize total sync time.

Cross-Platform Consistency

  • Normalize units: HealthKit returns distance in meters while Google Fit may vary. Always normalize to a consistent unit in your service layer.
  • Handle missing data uniformly: Both platforms may return empty arrays. Your summary logic should default to zero rather than crashing on undefined values.

Common Pitfalls

  1. Using Expo Go for testing: HealthKit and Google Fit native modules do not work in Expo Go. You must create a development build using eas build --profile development.

  2. Not accounting for timezone differences: HealthKit and Google Fit store dates in UTC. When querying for "today," always calculate the local start and end of day and convert to UTC.

  3. Assuming synchronous permission checks: Both platforms require async permission checks. Never block the UI thread waiting for permission status.

  4. Forgetting to handle Google Play Services unavailability: Some Android devices, particularly Huawei devices, do not have Google Play Services. Always call isAvailable() before using Google Fit.

  5. Not clearing the cache after permission changes: If the user revokes and re-grants permissions, your cached data may be stale. Clear the cache when permissions change.

Deploying to Production

  • Submit HealthKit entitlement justification to Apple App Review. Apple requires you to explain why your app needs each HealthKit data type.
  • Complete Google Fit OAuth consent screen verification in Google Cloud Console for production access.
  • Implement rate limiting on your sync engine to avoid hitting Google Fit API quotas.
  • Add E2E encryption for any health data that leaves the device and is sent to your backend.
  • Test permission flows on both platforms with fresh installs to ensure the first-run experience works correctly.

Complete Code

Key files in this implementation:

  • Shared types: src/types/health-data.ts
  • Service interface: src/services/HealthDataService.ts
  • iOS implementation: src/services/IOSHealthKitService.ts
  • Android implementation: src/services/AndroidGoogleFitService.ts
  • Platform factory: src/services/HealthServiceFactory.ts
  • React context: src/contexts/HealthDataContext.tsx
  • Example screen: src/screens/HealthDashboardScreen.tsx

Resources

#

Article Tags

React Native
Expo
HealthKit
Google Fit
Health Data

Found this article helpful?

Try KangXinBan and start your health management journey