跨平台移动医疗应用开发¶
学习目标¶
通过本文档的学习,你将能够:
- 理解核心概念和原理
- 掌握实际应用方法
- 了解最佳实践和注意事项
前置知识¶
在学习本文档之前,建议你已经掌握:
- 基础的嵌入式系统知识
- 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 账号登录。