康心伴Logo
康心伴WellAlly
Health

React Native Ex...?看这篇就够了 | WellAlly康心伴

5 分钟阅读

React Native Expo 集成 Apple HealthKit 和 Google Fit 健康数据指南

基于 CORE-EEAT 标准,结合最新 React Native 和 Expo SDK 实践

概述

在现代健康应用开发中,集成平台原生的健康数据服务(Apple HealthKit 和 Google Fit)是提供个性化健康体验的关键。本指南将详细介绍如何在 React Native Expo 应用中同时支持 iOS 和 Android 平台的健康数据获取。

核心优势

  • 跨平台统一接口: 使用相同的 API 获取两个平台的健康数据
  • 丰富数据类型: 支持步数、距离、卡路里、睡眠、心率等 50+ 种数据类型
  • 隐私合规: 自动处理平台权限请求和数据隐私合规
  • 离线同步: 支持离线数据存储和后台同步

技术架构

code
┌─────────────────────────────────────────────────────────────┐
│                    React Native 应用层                      │
├─────────────────────────────────────────────────────────────┤
│  HealthDataService (统一接口层)                             │
│  ┌─────────────────┐    ┌─────────────────┐                │
│  │  HealthKit API  │    │  Google Fit API │                │
│  │   (iOS)         │    │   (Android)     │                │
│  └─────────────────┘    └─────────────────┘                │
├─────────────────────────────────────────────────────────────┤
│  expo-health-kit (iOS)  │  react-native-google-fit (Android)│
└─────────────────────────────────────────────────────────────┘
Code collapsed

项目初始化

1. 创建 Expo 项目

code
npx create-expo-app@latest HealthTracker
cd HealthTracker
Code collapsed

2. 安装依赖

code
# 健康数据核心库
npx expo install expo-health-kit

# Google Fit (仅 Android)
npm install react-native-google-fit

# 其他依赖
npx expo install expo-sensors
npm install @react-native-async-storage/async-storage
Code collapsed

3. 配置 app.json/app.config.js

code
{
  "expo": {
    "name": "HealthTracker",
    "slug": "health-tracker",
    "version": "1.0.0",
    "orientation": "portrait",
    "icon": "./assets/icon.png",
    "userInterfaceStyle": "automatic",
    "splash": {
      "image": "./assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.yourcompany.healthtracker",
      "infoPlist": {
        "NSHealthShareUsageDescription": "此应用需要访问您的健康数据以追踪您的健身活动和健康指标。",
        "NSHealthUpdateUsageDescription": "此应用需要写入健康数据以保存您的健身记录。",
        "NSMotionUsageDescription": "此应用需要访问运动传感器数据以追踪您的活动。"
      },
      "entitlements": {
        "com.apple.developer.healthkit": true
      },
      "capabilities": [
        "com.apple.developer.healthkit"
      ]
    },
    "android": {
      "adaptiveIcon": {
        "foregroundImage": "./assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.yourcompany.healthtracker",
      "permissions": [
        "android.permission.ACTIVITY_RECOGNITION",
        "android.permission.BODY_SENSORS",
        "android.permission.ACCESS_FINE_LOCATION"
      ]
    },
    "plugins": [
      "expo-health-kit"
    ]
  }
}
Code collapsed

HealthKit 配置 (iOS)

1. 在 Apple Developer 配置

  1. 登录 Apple Developer
  2. 进入 App IDs 配置
  3. 启用 HealthKit Capability
  4. 重新生成 Provisioning Profile

2. 权限请求配置

code
// lib/healthKitConfig.ts
import { HealthKit } from 'expo-health-kit';

export const HEALTH_PERMISSIONS = {
  permissions: {
    read: [
      HealthKit.Constants.PermissionIdentifier.StepCount,
      HealthKit.Constants.PermissionIdentifier.DistanceWalkingRunning,
      HealthKit.Constants.PermissionIdentifier.ActiveEnergyBurned,
      HealthKit.Constants.PermissionIdentifier.HeartRate,
      HealthKit.Constants.PermissionIdentifier.SleepAnalysis,
      HealthKit.Constants.PermissionIdentifier.BloodPressure,
      HealthKit.Constants.PermissionIdentifier.BodyMassIndex,
      HealthKit.Constants.PermissionIdentifier.BodyMass,
      HealthKit.Constants.PermissionIdentifier.Height,
    ],
    write: [
      HealthKit.Constants.PermissionIdentifier.StepCount,
      HealthKit.Constants.PermissionIdentifier.ActiveEnergyBurned,
      HealthKit.Constants.PermissionIdentifier.DistanceWalkingRunning,
    ]
  }
};

export async function requestHealthKitPermission(): Promise<boolean> {
  const isAvailable = HealthKit.isAvailable();
  if (!isAvailable) {
    console.log('HealthKit is not available on this device');
    return false;
  }

  try {
    const permissions = await HealthKit.requestPermissions(HEALTH_PERMISSIONS.permissions);
    return permissions.read && permissions.write;
  } catch (error) {
    console.error('HealthKit permission error:', error);
    return false;
  }
}
Code collapsed

Google Fit 配置 (Android)

1. 在 Google Cloud Console 配置

  1. 创建或选择项目
  2. 启用 Fitness API
  3. 创建 OAuth 2.0 客户端 ID
  4. 添加 SHA-1 签名证书指纹
code
# 获取调试 SHA-1
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
Code collapsed

2. Google Fit 集成代码

code
// lib/googleFitConfig.ts
import { GoogleFit, Scopes } from 'react-native-google-fit';

export class GoogleFitService {
  private googleFit: GoogleFit;

  constructor() {
    this.googleFit = new GoogleFit();
  }

  async authorize(): Promise<boolean> {
    const options = {
      scopes: [
        Scopes.FITNESS_ACTIVITY_READ,
        Scopes.FITNESS_ACTIVITY_WRITE,
        Scopes.FITNESS_BODY_READ,
        Scopes.FITNESS_BODY_WRITE,
        Scopes.FITNESS_HEART_RATE_READ,
        Scopes.FITNESS_SLEEP_READ,
      ],
    };

    try {
      const authorized = await this.googleFit.authorize(options);
      return authorized.success;
    } catch (error) {
      console.error('Google Fit authorization error:', error);
      return false;
    }
  }

  async getSteps(startDate: Date, endDate: Date): Promise<number> {
    const options = {
      startDate: startDate.toISOString(),
      endDate: endDate.toISOString(),
      bucketUnit: 'DAY',
      bucketInterval: 1,
    };

    try {
      const result = await this.googleFit.getDailyStepCountSamples(options);
      return result.reduce((total, day) => total + (day.steps || 0), 0);
    } catch (error) {
      console.error('Google Fit get steps error:', error);
      return 0;
    }
  }

  async getHeartRate(startDate: Date, endDate: Date): Promise<number[]> {
    const options = {
      startDate: startDate.toISOString(),
      endDate: endDate.toISOString(),
      bucketUnit: 'DAY',
      bucketInterval: 1,
    };

    try {
      const result = await this.googleFit.getHeartRateSamples(options);
      return result.map(sample => sample.value);
    } catch (error) {
      console.error('Google Fit get heart rate error:', error);
      return [];
    }
  }
}
Code collapsed

统一健康数据服务

创建跨平台的统一接口,简化应用层的调用:

code
// lib/healthDataService.ts
import { Platform } from 'react-native';
import { HealthKit, DataType } from 'expo-health-kit';
import { GoogleFitService } from './googleFitConfig';

export interface HealthMetrics {
  steps: number;
  distance: number; // km
  calories: number; // kcal
  heartRate?: number;
  sleepHours?: number;
}

export class HealthDataService {
  private googleFit: GoogleFitService;
  private isAuthorized: boolean = false;

  constructor() {
    this.googleFit = new GoogleFitService();
  }

  async initialize(): Promise<boolean> {
    if (Platform.OS === 'ios') {
      this.isAuthorized = await this.requestIOSPermissions();
    } else {
      this.isAuthorized = await this.googleFit.authorize();
    }
    return this.isAuthorized;
  }

  private async requestIOSPermissions(): Promise<boolean> {
    if (!HealthKit.isAvailable()) return false;

    const permissions = {
      read: [
        HealthKit.Constants.PermissionIdentifier.StepCount,
        HealthKit.Constants.PermissionIdentifier.DistanceWalkingRunning,
        HealthKit.Constants.PermissionIdentifier.ActiveEnergyBurned,
        HealthKit.Constants.PermissionIdentifier.HeartRate,
        HealthKit.Constants.PermissionIdentifier.SleepAnalysis,
      ],
      write: [
        HealthKit.Constants.PermissionIdentifier.StepCount,
        HealthKit.Constants.PermissionIdentifier.ActiveEnergyBurned,
      ]
    };

    try {
      const result = await HealthKit.requestPermissions(permissions);
      return result.read && result.write;
    } catch (error) {
      console.error('iOS permission error:', error);
      return false;
    }
  }

  async getTodayMetrics(): Promise<HealthMetrics> {
    const now = new Date();
    const startOfDay = new Date(now.setHours(0, 0, 0, 0));
    const endOfDay = new Date();

    if (Platform.OS === 'ios') {
      return this.getIOSMetrics(startOfDay, endOfDay);
    } else {
      return this.getAndroidMetrics(startOfDay, endOfDay);
    }
  }

  private async getIOSMetrics(start: Date, end: Date): Promise<HealthMetrics> {
    const metrics: HealthMetrics = {
      steps: 0,
      distance: 0,
      calories: 0,
    };

    try {
      // 获取步数
      const steps = await HealthKit.queryQuantitySamples(
        HealthKit.Constants.PermissionIdentifier.StepCount,
        {
          startDate: start.toISOString(),
          endDate: end.toISOString(),
          limit: 1,
          aggregate: true,
        }
      );
      metrics.steps = steps[0]?.quantity || 0;

      // 获取距离 (米转公里)
      const distance = await HealthKit.queryQuantitySamples(
        HealthKit.Constants.PermissionIdentifier.DistanceWalkingRunning,
        {
          startDate: start.toISOString(),
          endDate: end.toISOString(),
          limit: 1,
          aggregate: true,
        }
      );
      metrics.distance = (distance[0]?.quantity || 0) / 1000;

      // 获取卡路里
      const calories = await HealthKit.queryQuantitySamples(
        HealthKit.Constants.PermissionIdentifier.ActiveEnergyBurned,
        {
          startDate: start.toISOString(),
          endDate: end.toISOString(),
          limit: 1,
          aggregate: true,
        }
      );
      metrics.calories = calories[0]?.quantity || 0;

      // 获取最新心率
      const heartRate = await HealthKit.queryQuantitySamples(
        HealthKit.Constants.PermissionIdentifier.HeartRate,
        {
          startDate: start.toISOString(),
          endDate: end.toISOString(),
          limit: 1,
          ascending: false,
        }
      );
      metrics.heartRate = heartRate[0]?.quantity;

    } catch (error) {
      console.error('iOS metrics error:', error);
    }

    return metrics;
  }

  private async getAndroidMetrics(start: Date, end: Date): Promise<HealthMetrics> {
    const metrics: HealthMetrics = {
      steps: 0,
      distance: 0,
      calories: 0,
    };

    try {
      // 获取步数
      metrics.steps = await this.googleFit.getSteps(start, end);

      // 获取其他指标...
      // Google Fit API 调用类似

    } catch (error) {
      console.error('Android metrics error:', error);
    }

    return metrics;
  }

  async writeSteps(steps: number): Promise<boolean> {
    const now = new Date();
    const start = new Date(now.getTime() - 60 * 60 * 1000); // 1小时前

    if (Platform.OS === 'ios') {
      try {
        await HealthKit.saveQuantitySample({
          identifier: HealthKit.Constants.PermissionIdentifier.StepCount,
          quantity: steps,
          startDate: start.toISOString(),
          endDate: now.toISOString(),
        });
        return true;
      } catch (error) {
        console.error('iOS write steps error:', error);
        return false;
      }
    } else {
      // Android 写入逻辑
      return true;
    }
  }
}
Code collapsed

React Hook 封装

code
// hooks/useHealthData.ts
import { useState, useEffect } from 'react';
import { HealthDataService, HealthMetrics } from '../lib/healthDataService';

export function useHealthData() {
  const [metrics, setMetrics] = useState<HealthMetrics>({
    steps: 0,
    distance: 0,
    calories: 0,
  });
  const [isLoading, setIsLoading] = useState(true);
  const [isAuthorized, setIsAuthorized] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const service = new HealthDataService();

    async function init() {
      try {
        const authorized = await service.initialize();
        setIsAuthorized(authorized);

        if (authorized) {
          const data = await service.getTodayMetrics();
          setMetrics(data);
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to load health data');
      } finally {
        setIsLoading(false);
      }
    }

    init();
  }, []);

  const refresh = async () => {
    setIsLoading(true);
    try {
      const service = new HealthDataService();
      const data = await service.getTodayMetrics();
      setMetrics(data);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to refresh data');
    } finally {
      setIsLoading(false);
    }
  };

  return { metrics, isLoading, isAuthorized, error, refresh };
}
Code collapsed

UI 组件示例

code
// components/HealthDashboard.tsx
import React from 'react';
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
import { useHealthData } from '../hooks/useHealthData';

export function HealthDashboard() {
  const { metrics, isLoading, isAuthorized, error } = useHealthData();

  if (!isAuthorized) {
    return (
      <View style={styles.container}>
        <Text style={styles.errorText}>
          需要健康数据权限才能显示此信息
        </Text>
      </View>
    );
  }

  if (isLoading) {
    return (
      <View style={styles.container}>
        <ActivityIndicator size="large" color="#4A90E2" />
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.container}>
        <Text style={styles.errorText}>错误: {error}</Text>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.title}>今日健康概览</Text>

      <View style={styles.metricCard}>
        <Text style={styles.metricValue}>{metrics.steps.toLocaleString()}</Text>
        <Text style={styles.metricLabel}>步数</Text>
      </View>

      <View style={styles.metricCard}>
        <Text style={styles.metricValue}>{metrics.distance.toFixed(2)}</Text>
        <Text style={styles.metricLabel}>公里</Text>
      </View>

      <View style={styles.metricCard}>
        <Text style={styles.metricValue}>{Math.round(metrics.calories)}</Text>
        <Text style={styles.metricLabel}>卡路里</Text>
      </View>

      {metrics.heartRate && (
        <View style={styles.metricCard}>
          <Text style={styles.metricValue}>{Math.round(metrics.heartRate)}</Text>
          <Text style={styles.metricLabel}>心率 (BPM)</Text>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
    textAlign: 'center',
  },
  metricCard: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 20,
    marginBottom: 12,
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  metricValue: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#4A90E2',
  },
  metricLabel: {
    fontSize: 16,
    color: '#666',
    marginTop: 4,
  },
  errorText: {
    color: 'red',
    textAlign: 'center',
  },
});
Code collapsed

平台注意事项

iOS 特定事项

  1. 隐私描述必须准确: App Store 审核时会检查权限描述是否与实际用途一致
  2. HealthKit 数据不能用于广告: 必须遵守 Apple 的健康数据使用政策
  3. 后台数据访问: 需要额外配置 UIBackgroundModes
code
// app.json - iOS 配置
{
  "ios": {
    "UIBackgroundModes": ["health-processing", "fetch"]
  }
}
Code collapsed

Android 特定事项

  1. OAuth 同意屏幕: 用户首次授权时会看到 Google 的同意屏幕
  2. API 配额: Google Fit API 有调用次数限制
  3. Google Play 服务: 需要设备安装最新的 Google Play 服务
code
// 检查 Google Play 服务可用性
import { checkPlayServices } from 'react-native-google-fit';

const result = await checkPlayServices();
if (!result) {
  Alert.alert('错误', '请更新 Google Play 服务');
}
Code collapsed

数据隐私最佳实践

  1. 最小权限原则: 只请求必要的健康数据权限
  2. 数据加密: 本地存储的健康数据应加密
  3. 用户控制: 提供清除数据的选项
  4. 透明度: 清晰说明数据用途
code
// 本地存储加密示例
import AsyncStorage from '@react-native-async-storage/async-storage';
import CryptoJS from 'crypto-js';

const SECRET_KEY = 'your-encryption-key';

export async function saveSecureData(key: string, data: any) {
  const encrypted = CryptoJS.AES.encrypt(
    JSON.stringify(data),
    SECRET_KEY
  ).toString();
  await AsyncStorage.setItem(key, encrypted);
}

export async function getSecureData(key: string) {
  const encrypted = await AsyncStorage.getItem(key);
  if (!encrypted) return null;

  const decrypted = CryptoJS.AES.decrypt(encrypted, SECRET_KEY);
  return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8));
}
Code collapsed

调试技巧

iOS 模拟器调试

code
# 在 iOS 模拟器中添加测试数据
# Debug > Location > City Run
Code collapsed

Android 模拟器调试

code
# 使用 ADB 添加测试步数
adb shell am broadcast \
  -a com.google.android.fitness.action.UPDATE_STEPS \
  --ei steps 10000
Code collapsed

参考资料


作者: 康心伴技术团队 | 发布日期: 2026年3月8日 | 最后更新: 2026年3月8日

免责声明: 本内容仅供教育参考,不能替代专业医疗建议。请咨询医生获取个性化诊断和治疗方案。

#

文章标签

health
wellness
react-native
expo
mobile-development

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅