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功能:

  1. 右键点击 “ios” 文件夹并选择 “Open in Xcode”。
  2. 在 “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

1 回复

更多关于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(),
              ),
      ),
    );
  }
}

在这个示例中,我们:

  1. 初始化了FlutterHealth实例。
  2. 请求了步数数据的访问权限。
  3. 如果权限被授予,则查询过去7天内的步数数据。
  4. 将查询结果显示在UI上。

确保在实际应用中处理权限请求被拒绝的情况,并为用户提供清晰的反馈。此外,你可能还需要根据具体需求调整数据查询的范围和类型。

回到顶部