Flutter健康数据访问插件flutter_health_wrapper的使用
Flutter健康数据访问插件flutter_health_wrapper的使用
flutter_health_wrapper
插件允许你从 Apple Health、Google Fit 和 Health Connect 读取和写入健康数据。该插件支持多种健康数据类型,并提供了处理权限、读取、写入和删除健康数据的方法。
数据类型
数据类型 | 单位 | iOS | Android (Google Fit) | Android (Health Connect) | 备注 |
---|---|---|---|---|---|
ACTIVE_ENERGY_BURNED | 卡路里 | 是 | 是 | 是 | |
BASAL_ENERGY_BURNED | 卡路里 | 是 | 否 | 否 | |
BLOOD_GLUCOSE | 毫克/分升 | 是 | 是 | 是 | |
BLOOD_OXYGEN | 百分比 | 是 | 是 | 是 | |
BLOOD_PRESSURE_DIASTOLIC | 毫米汞柱 | 是 | 是 | 是 | |
BLOOD_PRESSURE_SYSTOLIC | 毫米汞柱 | 是 | 是 | 是 | |
BODY_FAT_PERCENTAGE | 百分比 | 是 | 是 | 是 | |
BODY_MASS_INDEX | 无单位 | 是 | 是 | 是 | |
BODY_TEMPERATURE | 摄氏度 | 是 | 是 | 是 | |
ELECTRODERMAL_ACTIVITY | 西门子 | 是 | 否 | 否 | |
RESPIRATORY_RATE | 次/分钟 | 是 | 否 | 否 | |
PERIPHERAL_PERFUSION_INDEX | 百分比 | 是 | 否 | 否 | |
HEART_RATE | 次/分钟 | 是 | 是 | 是 | |
HEIGHT | 米 | 是 | 是 | 是 | |
RESTING_HEART_RATE | 次/分钟 | 是 | 否 | 否 | |
STEPS | 计数 | 是 | 是 | 是 | |
WAIST_CIRCUMFERENCE | 米 | 是 | 否 | 否 | |
WALKING_HEART_RATE | 次/分钟 | 是 | 否 | 否 | |
WEIGHT | 公斤 | 是 | 是 | 是 | |
DISTANCE_WALKING_RUNNING | 米 | 是 | 否 | 否 | |
FLIGHTS_CLIMBED | 计数 | 是 | 否 | 否 | |
MOVE_MINUTES | 分钟 | 否 | 是 | 否 | |
DISTANCE_DELTA | 米 | 否 | 是 | 是 | |
MINDFULNESS | 分钟 | 是 | 否 | 否 | |
SLEEP_IN_BED | 分钟 | 是 | 是 | 否 | |
SLEEP_ASLEEP | 分钟 | 是 | 是 | 否 | |
SLEEP_AWAKE | 分钟 | 是 | 是 | 否 | |
WATER | 升 | 是 | 是 | 是 | |
EXERCISE_TIME | 分钟 | 是 | 否 | 否 | |
WORKOUT | 无单位 | 是 | 是 | 是 | (有其他运动类型) |
HIGH_HEART_RATE_EVENT | 无单位 | 是 | 否 | 否 | 需要Apple Watch才能写入数据 |
LOW_HEART_RATE_EVENT | 无单位 | 是 | 否 | 否 | 需要Apple Watch才能写入数据 |
IRREGULAR_HEART_RATE_EVENT | 无单位 | 是 | 否 | 否 | 需要Apple Watch才能写入数据 |
HEART_RATE_VARIABILITY_SDNN | 毫秒 | 是 | 否 | 否 | 需要Apple Watch才能写入数据 |
HEADACHE_NOT_PRESENT | 分钟 | 是 | 否 | 否 | |
HEADACHE_MILD | 分钟 | 是 | 否 | 否 | |
HEADACHE_MODERATE | 分钟 | 是 | 否 | 否 | |
HEADACHE_SEVERE | 分钟 | 是 | 否 | 否 | |
HEADACHE_UNSPECIFIED | 分钟 | 是 | 否 | 否 | |
AUDIOGRAM | 分贝听力水平 | 是 | 否 | 否 | |
ELECTROCARDIOGRAM | 伏特 | 是 | 否 | 否 | 需要Apple Watch才能写入数据 |
设置
Apple Health (iOS)
步骤1: 修改Info.plist文件
在 Info.plist
文件中添加以下两个条目:
<key>NSHealthShareUsageDescription</key>
<string>我们将同步您的数据到Apple Health应用以给您更好的洞察。</string>
<key>NSHealthUpdateUsageDescription</key>
<string>我们将同步您的数据到Apple Health应用以给您更好的洞察。</string>
步骤2: 启用HealthKit
打开你的Flutter项目并启用HealthKit功能:
- 右键点击 “ios” 文件夹并选择 “Open in Xcode”。
- 在 “Signing & Capabilities” 标签页中为Runner目标添加HealthKit能力。
Google Fit (Android 选项1)
请参阅 Google Fit API文档 获取API密钥。
Health Connect (Android 选项2)
在 AndroidManifest.xml
文件中添加以下内容:
<queries>
<package android:name="com.google.android.apps.healthdata" />
</queries>
Android 权限
从API级别28(Android 9.0)开始,访问某些健身数据(如步数)需要特殊权限。请在 AndroidManifest.xml
文件中添加以下行:
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
如果使用Health Connect,还需要添加以下权限:
<uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
<uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
对于需要位置信息的运动类型,还需要以下权限:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
这些权限需要用户手动授权,可以使用 permission_handler
插件来请求权限:
await Permission.activityRecognition.request();
await Permission.location.request();
Android X
将 android/gradle.properties
文件内容替换为以下内容:
org.gradle.jvmargs=-Xmx1536M
android.enableJetifier=true
android.useAndroidX=true
使用
请查看示例应用程序以获取详细示例。
示例代码
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_health_wrapper/flutter_health_wrapper.dart';
import 'package:permission_handler/permission_handler.dart';
import 'util.dart';
void main() => runApp(HealthApp());
class HealthApp extends StatefulWidget {
[@override](/user/override)
_HealthAppState createState() => _HealthAppState();
}
enum AppState {
DATA_NOT_FETCHED,
FETCHING_DATA,
DATA_READY,
NO_DATA,
AUTHORIZED,
AUTH_NOT_GRANTED,
DATA_ADDED,
DATA_DELETED,
DATA_NOT_ADDED,
DATA_NOT_DELETED,
STEPS_READY,
}
class _HealthAppState extends State<HealthApp> {
List<HealthDataPoint> _healthDataList = [];
AppState _state = AppState.DATA_NOT_FETCHED;
int _nofSteps = 0;
// 定义要获取的数据类型
static final types = dataTypesIOS;
// 或者选择特定类型
// static final types = [
// HealthDataType.WEIGHT,
// HealthDataType.STEPS,
// HealthDataType.HEIGHT,
// HealthDataType.BLOOD_GLUCOSE,
// HealthDataType.WORKOUT,
// HealthDataType.BLOOD_PRESSURE_DIASTOLIC,
// HealthDataType.BLOOD_PRESSURE_SYSTOLIC,
// // 在iOS上取消注释以下行 - 只在iOS上可用
// // HealthDataType.AUDIOGRAM
// ];
// 对应的权限
// 只读
// final permissions = types.map((e) => HealthDataAccess.READ).toList();
// 或者读写
final permissions = types.map((e) => HealthDataAccess.READ).toList();
// 创建一个用于应用程序的HealthFactory实例
HealthFactory health = HealthFactory();
Future authorize() async {
// 如果我们尝试读取步数、运动等需要ACTIVITY_RECOGNITION权限的数据,我们需要先请求权限
// 这需要一个特殊的请求授权调用。
// 对于使用距离信息的运动类型,还需要请求位置权限。
await Permission.activityRecognition.request();
await Permission.location.request();
// 检查是否具有权限
bool? hasPermissions =
await health.hasPermissions(types, permissions: permissions);
// hasPermissions = false 因为hasPermission无法披露是否存在WRITE访问权限。
// 因此,我们需要使用WRITE权限进行请求。
hasPermissions = false;
bool authorized = false;
if (!hasPermissions) {
// 请求访问数据类型之前读取它们
try {
authorized =
await health.requestAuthorization(types, permissions: permissions);
} catch (error) {
print("Exception in authorize: $error");
}
}
setState(() => _state =
(authorized) ? AppState.AUTHORIZED : AppState.AUTH_NOT_GRANTED);
}
/// 从健康插件获取数据点并在应用程序中显示它们。
Future fetchData() async {
setState(() => _state = AppState.FETCHING_DATA);
// 获取过去24小时内的数据
final now = DateTime.now();
final yesterday = now.subtract(Duration(hours: 1300));
// 清除旧数据点
_healthDataList.clear();
try {
// 获取健康数据
List<HealthDataPoint> healthData =
await health.getHealthDataFromTypes(yesterday, now, types);
// 保存所有新数据点(仅前100个)
_healthDataList.addAll(
(healthData.length < 100) ? healthData : healthData.sublist(0, 100));
} catch (error) {
print("Exception in getHealthDataFromTypes: $error");
}
// 去重
_healthDataList = HealthFactory.removeDuplicates(_healthDataList);
// 打印结果
_healthDataList.forEach((x) => print(x));
// 更新UI以显示结果
setState(() {
_state = _healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY;
});
}
/// 添加一些随机健康数据。
Future addData() async {
final now = DateTime.now();
final earlier = now.subtract(Duration(minutes: 20));
// 添加受支持类型的数据
// 注意:这是Android的新API Health Connect所支持的类型。
// Android的Google Fit和iOS的HealthKit支持更多类型,我们在枚举列表 [HealthDataType] 中也支持。
// 添加更多 - 如AUDIOGRAM、HEADACHE_SEVERE等以进行测试。
bool success = true;
success &= await health.writeHealthData(
10, HealthDataType.BODY_FAT_PERCENTAGE, earlier, now);
success &= await health.writeHealthData(
1.925, HealthDataType.HEIGHT, earlier, now);
success &=
await health.writeHealthData(90, HealthDataType.WEIGHT, earlier, now);
success &= await health.writeHealthData(
90, HealthDataType.HEART_RATE, earlier, now);
success &=
await health.writeHealthData(90, HealthDataType.STEPS, earlier, now);
success &= await health.writeHealthData(
200, HealthDataType.ACTIVE_ENERGY_BURNED, earlier, now);
success &= await health.writeHealthData(
70, HealthDataType.HEART_RATE, earlier, now);
success &= await health.writeHealthData(
37, HealthDataType.BODY_TEMPERATURE, earlier, now);
success &= await health.writeBloodOxygen(98, earlier, now, flowRate: 1.0);
success &= await health.writeHealthData(
105, HealthDataType.BLOOD_GLUCOSE, earlier, now);
success &=
await health.writeHealthData(1.8, HealthDataType.WATER, earlier, now);
success &= await health.writeWorkoutData(
HealthWorkoutActivityType.AMERICAN_FOOTBALL,
now.subtract(Duration(minutes: 15)),
now,
totalDistance: 2430,
totalEnergyBurned: 400);
success &= await health.writeBloodPressure(90, 80, earlier, now);
// 存储一个音频图
// 在iOS上取消注释这些行 - 只在iOS上可用
// const frequencies = [125.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0];
// const leftEarSensitivities = [49.0, 54.0, 89.0, 52.0, 77.0, 35.0];
// const rightEarSensitivities = [76.0, 66.0, 90.0, 22.0, 85.0, 44.5];
// success &= await health.writeAudiogram(
// frequencies,
// leftEarSensitivities,
// rightEarSensitivities,
// now,
// now,
// metadata: {
// "HKExternalUUID": "uniqueID",
// "HKDeviceName": "bluetooth headphone",
// },
// );
setState(() {
_state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED;
});
}
/// 删除一些随机健康数据。
Future deleteData() async {
final now = DateTime.now();
final earlier = now.subtract(Duration(hours: 24));
bool success = true;
for (HealthDataType type in types) {
success &= await health.delete(type, earlier, now);
}
setState(() {
_state = success ? AppState.DATA_DELETED : AppState.DATA_NOT_DELETED;
});
}
/// 从健康插件获取步数并在应用程序中显示它们。
Future fetchStepData() async {
int? steps;
// 获取今天的步数(即从午夜开始)
final now = DateTime.now();
final midnight = DateTime(now.year, now.month, now.day);
bool requested = await health.requestAuthorization([HealthDataType.STEPS]);
if (requested) {
try {
steps = await health.getTotalStepsInInterval(midnight, now);
} catch (error) {
print("Caught exception in getTotalStepsInInterval: $error");
}
print('Total number of steps: $steps');
setState(() {
_nofSteps = (steps == null) ? 0 : steps;
_state = (steps == null) ? AppState.NO_DATA : AppState.STEPS_READY;
});
} else {
print("Authorization not granted - error in authorization");
setState(() => _state = AppState.DATA_NOT_FETCHED);
}
}
Future revokeAccess() async {
try {
await health.revokePermissions();
} catch (error) {
print("Caught exception in revokeAccess: $error");
}
}
Widget _contentFetchingData() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
padding: EdgeInsets.all(20),
child: CircularProgressIndicator(
strokeWidth: 10,
)),
Text('Fetching data...')
],
);
}
Widget _contentDataReady() {
return ListView.builder(
itemCount: _healthDataList.length,
itemBuilder: (_, index) {
HealthDataPoint p = _healthDataList[index];
if (p.value is AudiogramHealthValue) {
return ListTile(
title: Text("${p.typeString}: ${p.value}"),
trailing: Text('${p.unitString}'),
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
);
}
if (p.value is WorkoutHealthValue) {
return ListTile(
title: Text(
"${p.typeString}: ${(p.value as WorkoutHealthValue).totalEnergyBurned} ${(p.value as WorkoutHealthValue).totalEnergyBurnedUnit?.name}"),
trailing: Text(
'${(p.value as WorkoutHealthValue).workoutActivityType.name}'),
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
);
}
return ListTile(
title: Text("${p.typeString}: ${p.value}"),
trailing: Text('${p.unitString}'),
subtitle: Text('${p.dateFrom} - ${p.dateTo}'),
);
});
}
Widget _contentNoData() {
return Text('No Data to show');
}
Widget _contentNotFetched() {
return Column(
children: [
Text('按下载按钮获取数据。'),
Text('按加号按钮插入一些随机数据。'),
Text('按走路按钮获取总步数。'),
],
mainAxisAlignment: MainAxisAlignment.center,
);
}
Widget _authorized() {
return Text('授权成功!');
}
Widget _authorizationNotGranted() {
return Text('授权未给定。对于Android,请检查Google开发者控制台中的OAUTH2客户端ID是否正确。对于iOS,请检查Apple Health中的权限。');
}
Widget _dataAdded() {
return Text('数据点成功插入!');
}
Widget _dataDeleted() {
return Text('数据点成功删除!');
}
Widget _stepsFetched() {
return Text('总步数: $_nofSteps');
}
Widget _dataNotAdded() {
return Text('未能添加数据');
}
Widget _dataNotDeleted() {
return Text('未能删除数据');
}
Widget _content() {
if (_state == AppState.DATA_READY)
return _contentDataReady();
else if (_state == AppState.NO_DATA)
return _contentNoData();
else if (_state == AppState.FETCHING_DATA)
return _contentFetchingData();
else if (_state == AppState.AUTHORIZED)
return _authorized();
else if (_state == AppState.AUTH_NOT_GRANTED)
return _authorizationNotGranted();
else if (_state == AppState.DATA_ADDED)
return _dataAdded();
else if (_state == AppState.DATA_DELETED)
return _dataDeleted();
else if (_state == AppState.STEPS_READY)
return _stepsFetched();
else if (_state == AppState.DATA_NOT_ADDED)
return _dataNotAdded();
else if (_state == AppState.DATA_NOT_DELETED)
return _dataNotDeleted();
else
return _contentNotFetched();
}
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('健康示例'),
),
body: Container(
child: Column(
children: [
Wrap(
spacing: 10,
children: [
TextButton(
onPressed: authorize,
child:
Text("授权", style: TextStyle(color: Colors.white)),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
TextButton(
onPressed: fetchData,
child: Text("获取数据",
style: TextStyle(color: Colors.white)),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
TextButton(
onPressed: addData,
child: Text("添加数据",
style: TextStyle(color: Colors.white)),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
TextButton(
onPressed: deleteData,
child: Text("删除数据",
style: TextStyle(color: Colors.white)),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
TextButton(
onPressed: fetchStepData,
child: Text("获取步数数据",
style: TextStyle(color: Colors.white)),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
TextButton(
onPressed: revokeAccess,
child: Text("撤销授权",
style: TextStyle(color: Colors.white)),
style: ButtonStyle(
backgroundColor:
MaterialStatePropertyAll(Colors.blue))),
],
),
Divider(thickness: 3),
Expanded(child: Center(child: _content()))
],
),
),
),
);
}
}
更多关于Flutter健康数据访问插件flutter_health_wrapper的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter健康数据访问插件flutter_health_wrapper的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter项目中使用flutter_health_wrapper
插件来访问健康数据的示例代码。这个插件允许你读取和写入来自Android和iOS设备的健康数据。
首先,确保你已经在pubspec.yaml
文件中添加了flutter_health_wrapper
依赖:
dependencies:
flutter:
sdk: flutter
flutter_health_wrapper: ^x.y.z # 请替换为最新版本号
然后,运行flutter pub get
来安装依赖。
接下来,配置Android和iOS项目以访问健康数据。
Android配置
在android/app/src/main/AndroidManifest.xml
中添加必要的权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.yourapp">
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
<uses-permission android:name="android.permission.BODY_SENSORS"/>
<!-- 其他配置 -->
</manifest>
iOS配置
在ios/Runner/Info.plist
中添加必要的权限描述:
<key>NSHealthUpdateUsageDescription</key>
<string>Your app needs access to HealthKit to read your health data.</string>
<key>NSHealthShareUsageDescription</key>
<string>Your app needs access to HealthKit to save your health data.</string>
然后,在Xcode中,打开Runner
项目,导航到Signing & Capabilities
标签,并启用HealthKit
。
Flutter代码示例
以下是一个简单的Flutter应用示例,展示如何使用flutter_health_wrapper
插件来请求健康数据权限并读取步数数据:
import 'package:flutter/material.dart';
import 'package:flutter_health_wrapper/flutter_health_wrapper.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HealthDataScreen(),
);
}
}
class HealthDataScreen extends StatefulWidget {
@override
_HealthDataScreenState createState() => _HealthDataScreenState();
}
class _HealthDataScreenState extends State<HealthDataScreen> {
FlutterHealth? _health;
List<HealthDataType>? _dataTypes;
List<HealthDataRecord>? _stepData;
@override
void initState() {
super.initState();
initHealth();
}
Future<void> initHealth() async {
_health = FlutterHealth();
// 请求权限
bool isAuthorized = await _health!.requestAuthorization(
[HealthDataType.STEPS],
);
if (isAuthorized) {
// 读取数据
_stepData = await _health!.queryLatest(
dataType: HealthDataType.STEPS,
start: DateTime.now().subtract(Duration(days: 7)),
end: DateTime.now(),
);
setState(() {});
} else {
// 处理权限被拒绝的情况
print('Health data access denied');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Health Data Access'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: _stepData != null
? ListView.builder(
itemCount: _stepData!.length,
itemBuilder: (context, index) {
final record = _stepData![index];
return ListTile(
title: Text('Date: ${record.startDate!.toLocal()}'),
subtitle: Text('Steps: ${record.value!.toInt()}'),
);
},
)
: Center(
child: CircularProgressIndicator(),
),
),
);
}
}
在这个示例中,我们:
- 初始化了
FlutterHealth
实例。 - 请求了步数数据的访问权限。
- 如果权限被授予,则查询过去7天内的步数数据。
- 将查询结果显示在UI上。
确保在实际应用中处理权限请求被拒绝的情况,并为用户提供清晰的反馈。此外,你可能还需要根据具体需求调整数据查询的范围和类型。