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 配置
- 登录 Apple Developer
- 进入 App IDs 配置
- 启用 HealthKit Capability
- 重新生成 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 配置
- 创建或选择项目
- 启用 Fitness API
- 创建 OAuth 2.0 客户端 ID
- 添加 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 特定事项
- 隐私描述必须准确: App Store 审核时会检查权限描述是否与实际用途一致
- HealthKit 数据不能用于广告: 必须遵守 Apple 的健康数据使用政策
- 后台数据访问: 需要额外配置 UIBackgroundModes
code
// app.json - iOS 配置
{
"ios": {
"UIBackgroundModes": ["health-processing", "fetch"]
}
}
Code collapsed
Android 特定事项
- OAuth 同意屏幕: 用户首次授权时会看到 Google 的同意屏幕
- API 配额: Google Fit API 有调用次数限制
- 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
数据隐私最佳实践
- 最小权限原则: 只请求必要的健康数据权限
- 数据加密: 本地存储的健康数据应加密
- 用户控制: 提供清除数据的选项
- 透明度: 清晰说明数据用途
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
参考资料
- Expo HealthKit Documentation
- Apple HealthKit Documentation
- Google Fit API Documentation
- react-native-google-fit GitHub
作者: 康心伴技术团队 | 发布日期: 2026年3月8日 | 最后更新: 2026年3月8日