跳转至

跨平台移动医疗应用开发

学习目标

通过本文档的学习,你将能够:

  • 理解核心概念和原理
  • 掌握实际应用方法
  • 了解最佳实践和注意事项

前置知识

在学习本文档之前,建议你已经掌握:

  • 基础的嵌入式系统知识
  • C/C++编程基础
  • 相关领域的基本概念

概述

跨平台开发允许使用单一代码库为iOS和Android构建应用,可以显著降低开发成本和维护复杂度。本指南介绍主流跨平台框架在医疗应用开发中的应用。

主流框架对比

特性 React Native Flutter Xamarin
开发语言 JavaScript/TypeScript Dart C#
性能 良好 优秀 良好
UI渲染 原生组件 自绘引擎 原生组件
学习曲线 低(Web开发者) 中等 中等(.NET开发者)
社区支持 非常活跃 活跃 中等
热重载 支持 支持 有限支持
包大小 中等 较大 较大
医疗应用适用性 中等

React Native医疗应用开发

环境设置

# 安装React Native CLI
npm install -g react-native-cli

# 创建新项目
npx react-native init HealthApp --template react-native-template-typescript

# 安装必要依赖
npm install @react-navigation/native @react-navigation/stack
npm install react-native-health react-native-google-fit
npm install @react-native-async-storage/async-storage
npm install react-native-encrypted-storage

健康数据集成

iOS HealthKit集成

import AppleHealthKit, {
  HealthValue,
  HealthKitPermissions,
} from 'react-native-health';

const permissions: HealthKitPermissions = {
  permissions: {
    read: [
      AppleHealthKit.Constants.Permissions.HeartRate,
      AppleHealthKit.Constants.Permissions.Steps,
      AppleHealthKit.Constants.Permissions.BloodGlucose,
    ],
    write: [
      AppleHealthKit.Constants.Permissions.Weight,
    ],
  },
};

class HealthKitManager {
  // 初始化并请求权限
  static async initialize(): Promise<boolean> {
    return new Promise((resolve, reject) => {
      AppleHealthKit.initHealthKit(permissions, (error: string) => {
        if (error) {
          reject(error);
        } else {
          resolve(true);
        }
      });
    });
  }

  // 获取今日步数
  static async getTodaySteps(): Promise<number> {
    return new Promise((resolve, reject) => {
      const options = {
        date: new Date().toISOString(),
        includeManuallyAdded: false,
      };

      AppleHealthKit.getStepCount(options, (err: Object, results: HealthValue) => {
        if (err) {
          reject(err);
        } else {
          resolve(results.value);
        }
      });
    });
  }

  // 获取心率数据
  static async getHeartRateSamples(startDate: Date, endDate: Date): Promise<any[]> {
    return new Promise((resolve, reject) => {
      const options = {
        startDate: startDate.toISOString(),
        endDate: endDate.toISOString(),
        limit: 100,
      };

      AppleHealthKit.getHeartRateSamples(options, (err: Object, results: Array<HealthValue>) => {
        if (err) {
          reject(err);
        } else {
          resolve(results);
        }
      });
    });
  }

  // 保存体重
  static async saveWeight(weight: number): Promise<void> {
    return new Promise((resolve, reject) => {
      const options = {
        value: weight,
        date: new Date().toISOString(),
      };

      AppleHealthKit.saveWeight(options, (err: Object, result: any) => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });
  }
}

export default HealthKitManager;

Android Google Fit集成

import GoogleFit, { Scopes } from 'react-native-google-fit';

class GoogleFitManager {
  // 初始化并请求权限
  static async initialize(): Promise<boolean> {
    const options = {
      scopes: [
        Scopes.FITNESS_ACTIVITY_READ,
        Scopes.FITNESS_ACTIVITY_WRITE,
        Scopes.FITNESS_BODY_READ,
        Scopes.FITNESS_BODY_WRITE,
      ],
    };

    return GoogleFit.authorize(options);
  }

  // 获取今日步数
  static async getTodaySteps(): Promise<number> {
    const opt = {
      startDate: new Date().setHours(0, 0, 0, 0),
      endDate: new Date().valueOf(),
    };

    const result = await GoogleFit.getDailyStepCountSamples(opt);

    if (result.length > 0) {
      const steps = result[0].steps.reduce((total: number, step: any) => {
        return total + step.value;
      }, 0);
      return steps;
    }

    return 0;
  }

  // 获取心率数据
  static async getHeartRateSamples(startDate: Date, endDate: Date): Promise<any[]> {
    const options = {
      startDate: startDate.toISOString(),
      endDate: endDate.toISOString(),
    };

    return GoogleFit.getHeartRateSamples(options);
  }

  // 保存体重
  static async saveWeight(weight: number): Promise<void> {
    const options = {
      value: weight,
      date: new Date().toISOString(),
      unit: 'kg',
    };

    return GoogleFit.saveWeight(options);
  }
}

export default GoogleFitManager;

UI组件示例

import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  Platform,
  ActivityIndicator,
} from 'react-native';
import HealthKitManager from './HealthKitManager';
import GoogleFitManager from './GoogleFitManager';

interface HealthMetric {
  title: string;
  value: string;
  unit: string;
  icon: string;
}

const HealthDashboard: React.FC = () => {
  const [loading, setLoading] = useState(true);
  const [metrics, setMetrics] = useState<HealthMetric[]>([]);

  useEffect(() => {
    initializeHealth();
  }, []);

  const initializeHealth = async () => {
    try {
      if (Platform.OS === 'ios') {
        await HealthKitManager.initialize();
        await loadIOSHealthData();
      } else {
        await GoogleFitManager.initialize();
        await loadAndroidHealthData();
      }
    } catch (error) {
      console.error('初始化失败:', error);
    } finally {
      setLoading(false);
    }
  };

  const loadIOSHealthData = async () => {
    const steps = await HealthKitManager.getTodaySteps();
    const heartRates = await HealthKitManager.getHeartRateSamples(
      new Date(Date.now() - 24 * 60 * 60 * 1000),
      new Date()
    );

    const avgHeartRate = heartRates.length > 0
      ? heartRates.reduce((sum, hr) => sum + hr.value, 0) / heartRates.length
      : 0;

    setMetrics([
      { title: '步数', value: steps.toString(), unit: '步', icon: '🚶' },
      { title: '心率', value: Math.round(avgHeartRate).toString(), unit: 'bpm', icon: '❤️' },
    ]);
  };

  const loadAndroidHealthData = async () => {
    const steps = await GoogleFitManager.getTodaySteps();
    const heartRates = await GoogleFitManager.getHeartRateSamples(
      new Date(Date.now() - 24 * 60 * 60 * 1000),
      new Date()
    );

    const avgHeartRate = heartRates.length > 0
      ? heartRates.reduce((sum: number, hr: any) => sum + hr.value, 0) / heartRates.length
      : 0;

    setMetrics([
      { title: '步数', value: steps.toString(), unit: '步', icon: '🚶' },
      { title: '心率', value: Math.round(avgHeartRate).toString(), unit: 'bpm', icon: '❤️' },
    ]);
  };

  if (loading) {
    return (
      <View style={styles.loadingContainer}>
        <ActivityIndicator size="large" color="#007AFF" />
      </View>
    );
  }

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.title}>健康数据</Text>
      {metrics.map((metric, index) => (
        <View key={index} style={styles.card}>
          <Text style={styles.icon}>{metric.icon}</Text>
          <View style={styles.cardContent}>
            <Text style={styles.metricTitle}>{metric.title}</Text>
            <Text style={styles.metricValue}>
              {metric.value} <Text style={styles.unit}>{metric.unit}</Text>
            </Text>
          </View>
        </View>
      ))}
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
    padding: 16,
  },
  loadingContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 20,
    color: '#333',
  },
  card: {
    backgroundColor: 'white',
    borderRadius: 12,
    padding: 20,
    marginBottom: 16,
    flexDirection: 'row',
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  icon: {
    fontSize: 40,
    marginRight: 16,
  },
  cardContent: {
    flex: 1,
  },
  metricTitle: {
    fontSize: 16,
    color: '#666',
    marginBottom: 4,
  },
  metricValue: {
    fontSize: 32,
    fontWeight: 'bold',
    color: '#333',
  },
  unit: {
    fontSize: 16,
    fontWeight: 'normal',
    color: '#999',
  },
});

export default HealthDashboard;

Flutter医疗应用开发

环境设置

# 安装Flutter
# 访问 https://flutter.dev/docs/get-started/install

# 创建新项目
flutter create health_app
cd health_app

# 添加依赖到 pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  health: ^4.5.0
  provider: ^6.0.5
  http: ^0.13.5
  flutter_secure_storage: ^8.0.0
  intl: ^0.18.0

健康数据集成

import 'package:health/health.dart';
import 'package:permission_handler/permission_handler.dart';

class HealthDataManager {
  final HealthFactory health = HealthFactory();

  // 定义需要的数据类型
  static final List<HealthDataType> types = [
    HealthDataType.STEPS,
    HealthDataType.HEART_RATE,
    HealthDataType.BLOOD_GLUCOSE,
    HealthDataType.BLOOD_PRESSURE_SYSTOLIC,
    HealthDataType.BLOOD_PRESSURE_DIASTOLIC,
    HealthDataType.WEIGHT,
  ];

  // 请求权限
  Future<bool> requestAuthorization() async {
    bool? hasPermissions = await health.hasPermissions(types);

    if (hasPermissions == false) {
      hasPermissions = await health.requestAuthorization(types);
    }

    return hasPermissions ?? false;
  }

  // 获取今日步数
  Future<int> getTodaySteps() async {
    final now = DateTime.now();
    final midnight = DateTime(now.year, now.month, now.day);

    final steps = await health.getTotalStepsInInterval(midnight, now);
    return steps ?? 0;
  }

  // 获取心率数据
  Future<List<HealthDataPoint>> getHeartRateData(
    DateTime startDate,
    DateTime endDate,
  ) async {
    final healthData = await health.getHealthDataFromTypes(
      startDate,
      endDate,
      [HealthDataType.HEART_RATE],
    );

    return healthData;
  }

  // 写入体重数据
  Future<bool> writeWeight(double weight) async {
    final now = DateTime.now();

    return await health.writeHealthData(
      weight,
      HealthDataType.WEIGHT,
      now,
      now,
    );
  }

  // 写入血压数据
  Future<bool> writeBloodPressure(int systolic, int diastolic) async {
    final now = DateTime.now();

    final systolicSuccess = await health.writeHealthData(
      systolic.toDouble(),
      HealthDataType.BLOOD_PRESSURE_SYSTOLIC,
      now,
      now,
    );

    final diastolicSuccess = await health.writeHealthData(
      diastolic.toDouble(),
      HealthDataType.BLOOD_PRESSURE_DIASTOLIC,
      now,
      now,
    );

    return systolicSuccess && diastolicSuccess;
  }

  // 获取一周的步数统计
  Future<Map<DateTime, int>> getWeeklySteps() async {
    final now = DateTime.now();
    final weekAgo = now.subtract(Duration(days: 7));

    Map<DateTime, int> dailySteps = {};

    for (int i = 0; i < 7; i++) {
      final date = weekAgo.add(Duration(days: i));
      final startOfDay = DateTime(date.year, date.month, date.day);
      final endOfDay = startOfDay.add(Duration(days: 1));

      final steps = await health.getTotalStepsInInterval(startOfDay, endOfDay);
      dailySteps[startOfDay] = steps ?? 0;
    }

    return dailySteps;
  }
}

UI组件示例

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'health_data_manager.dart';

class HealthDashboardScreen extends StatefulWidget {
  @override
  _HealthDashboardScreenState createState() => _HealthDashboardScreenState();
}

class _HealthDashboardScreenState extends State<HealthDashboardScreen> {
  final HealthDataManager _healthManager = HealthDataManager();
  bool _isLoading = true;
  int _steps = 0;
  double _avgHeartRate = 0;

  @override
  void initState() {
    super.initState();
    _initializeHealth();
  }

  Future<void> _initializeHealth() async {
    try {
      final authorized = await _healthManager.requestAuthorization();

      if (authorized) {
        await _loadHealthData();
      }
    } catch (e) {
      print('初始化失败: $e');
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _loadHealthData() async {
    final steps = await _healthManager.getTodaySteps();

    final now = DateTime.now();
    final yesterday = now.subtract(Duration(days: 1));
    final heartRateData = await _healthManager.getHeartRateData(yesterday, now);

    double avgHeartRate = 0;
    if (heartRateData.isNotEmpty) {
      final sum = heartRateData.fold<double>(
        0,
        (prev, data) => prev + (data.value as num).toDouble(),
      );
      avgHeartRate = sum / heartRateData.length;
    }

    setState(() {
      _steps = steps;
      _avgHeartRate = avgHeartRate;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: Text('健康数据'),
        elevation: 0,
      ),
      body: RefreshIndicator(
        onRefresh: _loadHealthData,
        child: ListView(
          padding: EdgeInsets.all(16),
          children: [
            _buildMetricCard(
              title: '步数',
              value: _steps.toString(),
              unit: '步',
              icon: Icons.directions_walk,
              color: Colors.green,
            ),
            SizedBox(height: 16),
            _buildMetricCard(
              title: '平均心率',
              value: _avgHeartRate.round().toString(),
              unit: 'bpm',
              icon: Icons.favorite,
              color: Colors.red,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildMetricCard({
    required String title,
    required String value,
    required String unit,
    required IconData icon,
    required Color color,
  }) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      child: Padding(
        padding: EdgeInsets.all(20),
        child: Row(
          children: [
            Container(
              padding: EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: color.withOpacity(0.1),
                borderRadius: BorderRadius.circular(12),
              ),
              child: Icon(
                icon,
                size: 32,
                color: color,
              ),
            ),
            SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: TextStyle(
                      fontSize: 16,
                      color: Colors.grey[600],
                    ),
                  ),
                  SizedBox(height: 4),
                  Row(
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      Text(
                        value,
                        style: TextStyle(
                          fontSize: 32,
                          fontWeight: FontWeight.bold,
                          color: Colors.black87,
                        ),
                      ),
                      SizedBox(width: 4),
                      Padding(
                        padding: EdgeInsets.only(bottom: 4),
                        child: Text(
                          unit,
                          style: TextStyle(
                            fontSize: 16,
                            color: Colors.grey[600],
                          ),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

跨平台开发最佳实践

1. 平台特定代码隔离

// React Native示例
import { Platform } from 'react-native';

const HealthManager = Platform.select({
  ios: () => require('./HealthKitManager').default,
  android: () => require('./GoogleFitManager').default,
})();

export default HealthManager;

2. 统一的数据模型

interface HealthData {
  type: 'steps' | 'heartRate' | 'weight' | 'bloodPressure';
  value: number;
  unit: string;
  timestamp: Date;
  source: string;
}

class HealthDataAdapter {
  static normalize(platformData: any, platform: 'ios' | 'android'): HealthData {
    // 将平台特定数据转换为统一格式
    return {
      type: platformData.type,
      value: platformData.value,
      unit: platformData.unit,
      timestamp: new Date(platformData.timestamp),
      source: platform,
    };
  }
}

3. 错误处理

class HealthDataException implements Exception {
  final String message;
  final String? code;

  HealthDataException(this.message, [this.code]);

  @override
  String toString() => 'HealthDataException: $message (code: $code)';
}

Future<T> handleHealthOperation<T>(Future<T> Function() operation) async {
  try {
    return await operation();
  } on PlatformException catch (e) {
    throw HealthDataException(
      '平台错误: ${e.message}',
      e.code,
    );
  } catch (e) {
    throw HealthDataException('未知错误: $e');
  }
}

性能优化

1. 数据缓存

class HealthDataCache {
  private cache: Map<string, { data: any; timestamp: number }> = new Map();
  private readonly TTL = 5 * 60 * 1000; // 5分钟

  set(key: string, data: any): void {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
    });
  }

  get(key: string): any | null {
    const cached = this.cache.get(key);

    if (!cached) return null;

    if (Date.now() - cached.timestamp > this.TTL) {
      this.cache.delete(key);
      return null;
    }

    return cached.data;
  }

  clear(): void {
    this.cache.clear();
  }
}

2. 批量数据处理

class BatchHealthDataProcessor {
  static const int BATCH_SIZE = 100;

  Future<void> processBatch(List<HealthDataPoint> data) async {
    for (int i = 0; i < data.length; i += BATCH_SIZE) {
      final end = (i + BATCH_SIZE < data.length) ? i + BATCH_SIZE : data.length;
      final batch = data.sublist(i, end);

      await _processBatchData(batch);

      // 避免阻塞UI
      await Future.delayed(Duration(milliseconds: 10));
    }
  }

  Future<void> _processBatchData(List<HealthDataPoint> batch) async {
    // 处理批量数据
  }
}

测试策略

React Native测试

import { render, waitFor } from '@testing-library/react-native';
import HealthDashboard from './HealthDashboard';
import HealthKitManager from './HealthKitManager';

jest.mock('./HealthKitManager');

describe('HealthDashboard', () => {
  it('displays health metrics', async () => {
    (HealthKitManager.getTodaySteps as jest.Mock).mockResolvedValue(5000);

    const { getByText } = render(<HealthDashboard />);

    await waitFor(() => {
      expect(getByText('5000')).toBeTruthy();
    });
  });
});

Flutter测试

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'health_data_manager.dart';

class MockHealthDataManager extends Mock implements HealthDataManager {}

void main() {
  group('HealthDataManager', () {
    test('getTodaySteps returns correct value', () async {
      final mockManager = MockHealthDataManager();
      when(mockManager.getTodaySteps()).thenAnswer((_) async => 5000);

      final steps = await mockManager.getTodaySteps();
      expect(steps, 5000);
    });
  });
}

相关资源

下一步


最后更新: 2024年


💬 讨论区

欢迎在这里分享您的想法、提出问题或参与讨论。需要 GitHub 账号登录。