Flutter健康数据访问插件health的使用

发布于 1周前 作者 nodeper 来自 Flutter

Flutter健康数据访问插件health的使用

插件简介

Health 插件允许从 Apple HealthGoogle Health Connect 读取和写入健康数据。请注意,自2024年5月1日起,Google已停止对Google Fit API的支持,因此该插件自11.0.0版本起移除了对Google Fit的支持。

支持的功能

  • 权限管理:通过hasPermissionsrequestAuthorizationrevokePermissions方法处理健康数据访问权限。
  • 读取健康数据:使用getHealthDataFromTypes方法读取数据。
  • 写入健康数据:使用writeHealthData方法写入数据。
  • 写入锻炼数据:使用writeWorkout方法写入锻炼记录。
  • 写入膳食数据:在iOS和Android上使用writeMeal方法写入膳食信息。
  • 写入听力图(iOS):使用writeAudiogram方法写入听力图。
  • 写入血压数据:使用writeBloodPressure方法写入血压信息。
  • 获取步数统计:使用getTotalStepsInInterval方法获取步数。
  • 清理重复数据点:通过removeDuplicates方法清理重复数据。
  • 删除特定时间段的数据:使用delete方法删除指定类型的数据。

对于Android设备,目标手机需要安装Health Connect应用,并且能够连接互联网。

设置

Apple Health (iOS)

  1. Info.plist中添加以下两项:

    <key>NSHealthShareUsageDescription</key>
    <string>我们将同步您的数据到Apple Health应用程序,以提供更好的见解</string>
    <key>NSHealthUpdateUsageDescription</key>
    <string>我们将同步您的数据到Apple Health应用程序,以提供更好的见解</string>
    
  2. 打开Flutter项目,在Xcode中为Runner target启用“HealthKit”功能。

Google Health Connect (Android)

  1. AndroidManifest.xml中添加以下代码段,以检查是否安装了Health Connect应用:

    <queries>
      <package android:name="com.google.android.apps.healthdata" />
      <intent>
        <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
      </intent>
    </queries>
    
  2. 添加隐私政策链接(可选但推荐):

    <activity-alias
        android:name="ViewPermissionUsageActivity"
        android:exported="true"
        android:targetActivity=".MainActivity"
        android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
        <intent-filter>
            <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
            <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
        </intent-filter>
    </activity-alias>
    
  3. 对于每个要访问的数据类型,在AndroidManifest.xml中添加相应的读写权限。例如,访问心率数据:

    <uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
    <uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
    
  4. 如果需要访问健身数据(如步数),需添加活动识别API权限:

    <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
    
  5. 如果请求锻炼距离,则需添加位置权限:

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    
  6. 使用permission_handler插件请求上述权限:

    await Permission.activityRecognition.request();
    await Permission.location.request();
    
  7. 添加意图过滤器以显示Health Connect权限界面:

    <activity
      android:name=".MainActivity"
      android:exported="true">
      ...
      <intent-filter>
        <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
      </intent-filter>
    </activity>
    
  8. 对于Android 14,确保主活动继承自FlutterFragmentActivity

    import io.flutter.embedding.android.FlutterFragmentActivity
    
    class MainActivity : FlutterFragmentActivity() {
        ...
    }
    
  9. 确保android/gradle.properties文件包含以下内容:

    org.gradle.jvmargs=-Xmx1536M
    android.enableJetifier=true
    android.useAndroidX=true
    

使用示例

以下是一个完整的Flutter应用程序示例,演示如何配置和使用Health插件。

import 'dart:async';
import 'dart:io';

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

// 全局Health实例
final health = Health();

void main() => runApp(HealthApp());

class HealthApp extends StatefulWidget {
  const HealthApp({super.key});

  @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,
  HEALTH_CONNECT_STATUS,
  PERMISSIONS_REVOKING,
  PERMISSIONS_REVOKED,
  PERMISSIONS_NOT_REVOKED,
}

class HealthAppState extends State<HealthApp> {
  List<HealthDataPoint> _healthDataList = [];
  AppState _state = AppState.DATA_NOT_FETCHED;
  int _nofSteps = 0;
  List<RecordingMethod> recordingMethodsToFilter = [];

  // 所有平台支持的数据类型
  List<HealthDataType> get types => (Platform.isAndroid)
      ? dataTypesAndroid
      : (Platform.isIOS)
          ? dataTypesIOS
          : [];

  // 权限设置
  List<HealthDataAccess> get permissions => types
      .map((type) =>
          [
            HealthDataType.WALKING_HEART_RATE,
            HealthDataType.ELECTROCARDIOGRAM,
            HealthDataType.HIGH_HEART_RATE_EVENT,
            HealthDataType.LOW_HEART_RATE_EVENT,
            HealthDataType.IRREGULAR_HEART_RATE_EVENT,
            HealthDataType.EXERCISE_TIME,
          ].contains(type)
              ? HealthDataAccess.READ
              : HealthDataAccess.READ_WRITE)
      .toList();

  @override
  void initState() {
    health.configure();
    health.getHealthConnectSdkStatus();
    super.initState();
  }

  /// 安装Health Connect应用
  Future<void> installHealthConnect() async =>
      await health.installHealthConnect();

  /// 授权访问健康数据
  Future<void> authorize() async {
    await Permission.activityRecognition.request();
    await Permission.location.request();

    bool? hasPermissions =
        await health.hasPermissions(types, permissions: permissions);

    hasPermissions = false;

    bool authorized = false;
    if (!hasPermissions) {
      try {
        authorized =
            await health.requestAuthorization(types, permissions: permissions);
      } catch (error) {
        debugPrint("Exception in authorize: $error");
      }
    }

    setState(() =>
        _state = (authorized) ? AppState.AUTHORIZED : AppState.AUTH_NOT_GRANTED);
  }

  /// 获取Health Connect状态
  Future<void> getHealthConnectSdkStatus() async {
    assert(Platform.isAndroid, "This is only available on Android");

    final status = await health.getHealthConnectSdkStatus();

    setState(() {
      _contentHealthConnectStatus =
          Text('Health Connect Status: ${status?.name.toUpperCase()}');
      _state = AppState.HEALTH_CONNECT_STATUS;
    });
  }

  /// 获取健康数据
  Future<void> fetchData() async {
    setState(() => _state = AppState.FETCHING_DATA);

    final now = DateTime.now();
    final yesterday = now.subtract(const Duration(hours: 24));

    _healthDataList.clear();

    try {
      List<HealthDataPoint> healthData = await health.getHealthDataFromTypes(
        types: types,
        startTime: yesterday,
        endTime: now,
        recordingMethodsToFilter: recordingMethodsToFilter,
      );

      healthData.sort((a, b) => b.dateTo.compareTo(a.dateTo));
      _healthDataList.addAll(
          (healthData.length < 100) ? healthData : healthData.sublist(0, 100));
    } catch (error) {
      debugPrint("Exception in getHealthDataFromTypes: $error");
    }

    _healthDataList = health.removeDuplicates(_healthDataList);

    for (var data in _healthDataList) {
      debugPrint(toJsonString(data));
    }

    setState(() {
      _state = _healthDataList.isEmpty ? AppState.NO_DATA : AppState.DATA_READY;
    });
  }

  /// 添加随机健康数据
  Future<void> addData() async {
    final now = DateTime.now();
    final earlier = now.subtract(const Duration(minutes: 20));

    bool success = true;

    success &= await health.writeHealthData(
        value: 1.925,
        type: HealthDataType.HEIGHT,
        startTime: earlier,
        endTime: now,
        recordingMethod: RecordingMethod.manual);
    success &= await health.writeHealthData(
        value: 90,
        type: HealthDataType.WEIGHT,
        startTime: now,
        recordingMethod: RecordingMethod.manual);
    success &= await health.writeHealthData(
        value: 90,
        type: HealthDataType.HEART_RATE,
        startTime: earlier,
        endTime: now,
        recordingMethod: RecordingMethod.manual);
    success &= await health.writeHealthData(
        value: 90,
        type: HealthDataType.STEPS,
        startTime: earlier,
        endTime: now,
        recordingMethod: RecordingMethod.manual);
    success &= await health.writeHealthData(
      value: 200,
      type: HealthDataType.ACTIVE_ENERGY_BURNED,
      startTime: earlier,
      endTime: now,
    );
    success &= await health.writeHealthData(
        value: 70,
        type: HealthDataType.HEART_RATE,
        startTime: earlier,
        endTime: now);
    if (Platform.isIOS) {
      success &= await health.writeHealthData(
          value: 30,
          type: HealthDataType.HEART_RATE_VARIABILITY_SDNN,
          startTime: earlier,
          endTime: now);
    } else {
      success &= await health.writeHealthData(
          value: 30,
          type: HealthDataType.HEART_RATE_VARIABILITY_RMSSD,
          startTime: earlier,
          endTime: now);
    }
    success &= await health.writeHealthData(
        value: 37,
        type: HealthDataType.BODY_TEMPERATURE,
        startTime: earlier,
        endTime: now);
    success &= await health.writeHealthData(
        value: 105,
        type: HealthDataType.BLOOD_GLUCOSE,
        startTime: earlier,
        endTime: now);
    success &= await health.writeHealthData(
        value: 1.8,
        type: HealthDataType.WATER,
        startTime: earlier,
        endTime: now);

    success &= await health.writeHealthData(
        value: 0.0,
        type: HealthDataType.SLEEP_REM,
        startTime: earlier,
        endTime: now);
    success &= await health.writeHealthData(
        value: 0.0,
        type: HealthDataType.SLEEP_ASLEEP,
        startTime: earlier,
        endTime: now);
    success &= await health.writeHealthData(
        value: 0.0,
        type: HealthDataType.SLEEP_AWAKE,
        startTime: earlier,
        endTime: now);
    success &= await health.writeHealthData(
        value: 0.0,
        type: HealthDataType.SLEEP_DEEP,
        startTime: earlier,
        endTime: now);
    success &= await health.writeHealthData(
      value: 22,
      type: HealthDataType.LEAN_BODY_MASS,
      startTime: earlier,
      endTime: now,
    );

    success &= await health.writeBloodOxygen(
      saturation: 98,
      startTime: earlier,
      endTime: now,
    );
    success &= await health.writeWorkoutData(
      activityType: HealthWorkoutActivityType.AMERICAN_FOOTBALL,
      title: "Random workout name that shows up in Health Connect",
      start: now.subtract(const Duration(minutes: 15)),
      end: now,
      totalDistance: 2430,
      totalEnergyBurned: 400,
    );
    success &= await health.writeBloodPressure(
      systolic: 90,
      diastolic: 80,
      startTime: now,
    );
    success &= await health.writeMeal(
        mealType: MealType.SNACK,
        startTime: earlier,
        endTime: now,
        caloriesConsumed: 1000,
        carbohydrates: 50,
        protein: 25,
        fatTotal: 50,
        name: "Banana",
        caffeine: 0.002,
        vitaminA: 0.001,
        vitaminC: 0.002,
        vitaminD: 0.003,
        vitaminE: 0.004,
        vitaminK: 0.005,
        b1Thiamin: 0.006,
        b2Riboflavin: 0.007,
        b3Niacin: 0.008,
        b5PantothenicAcid: 0.009,
        b6Pyridoxine: 0.010,
        b7Biotin: 0.011,
        b9Folate: 0.012,
        b12Cobalamin: 0.013,
        calcium: 0.015,
        copper: 0.016,
        iodine: 0.017,
        iron: 0.018,
        magnesium: 0.019,
        manganese: 0.020,
        phosphorus: 0.021,
        potassium: 0.022,
        selenium: 0.023,
        sodium: 0.024,
        zinc: 0.025,
        water: 0.026,
        molybdenum: 0.027,
        chloride: 0.028,
        chromium: 0.029,
        cholesterol: 0.030,
        fiber: 0.031,
        fatMonounsaturated: 0.032,
        fatPolyunsaturated: 0.033,
        fatUnsaturated: 0.065,
        fatTransMonoenoic: 0.65,
        fatSaturated: 066,
        sugar: 0.067,
        recordingMethod: RecordingMethod.manual);

    success &= await health.writeMenstruationFlow(
      flow: MenstrualFlow.medium,
      isStartOfCycle: true,
      startTime: earlier,
      endTime: now,
    );

    if (Platform.isIOS) {
      success &= await health.writeHealthData(
          value: 22,
          type: HealthDataType.WATER_TEMPERATURE,
          startTime: earlier,
          endTime: now,
          recordingMethod: RecordingMethod.manual);

      success &= await health.writeHealthData(
          value: 55,
          type: HealthDataType.UNDERWATER_DEPTH,
          startTime: earlier,
          endTime: now,
          recordingMethod: RecordingMethod.manual);
    }

    setState(() {
      _state = success ? AppState.DATA_ADDED : AppState.DATA_NOT_ADDED;
    });
  }

  /// 删除随机健康数据
  Future<void> deleteData() async {
    final now = DateTime.now();
    final earlier = now.subtract(const Duration(hours: 24));

    bool success = true;
    for (HealthDataType type in types) {
      success &= await health.delete(
        type: type,
        startTime: earlier,
        endTime: now,
      );
    }

    setState(() {
      _state = success ? AppState.DATA_DELETED : AppState.DATA_NOT_DELETED;
    });
  }

  /// 获取步数数据
  Future<void> fetchStepData() async {
    int? steps;

    final now = DateTime.now();
    final midnight = DateTime(now.year, now.month, now.day);

    bool stepsPermission =
        await health.hasPermissions([HealthDataType.STEPS]) ?? false;
    if (!stepsPermission) {
      stepsPermission =
          await health.requestAuthorization([HealthDataType.STEPS]);
    }

    if (stepsPermission) {
      try {
        steps = await health.getTotalStepsInInterval(midnight, now,
            includeManualEntry:
                !recordingMethodsToFilter.contains(RecordingMethod.manual));
      } catch (error) {
        debugPrint("Exception in getTotalStepsInInterval: $error");
      }

      setState(() {
        _nofSteps = (steps == null) ? 0 : steps;
        _state = (steps == null) ? AppState.NO_DATA : AppState.STEPS_READY;
      });
    } else {
      setState(() => _state = AppState.DATA_NOT_FETCHED);
    }
  }

  /// 撤销健康数据访问权限
  Future<void> revokeAccess() async {
    setState(() => _state = AppState.PERMISSIONS_REVOKING);

    bool success = false;

    try {
      await health.revokePermissions();
      success = true;
    } catch (error) {
      debugPrint("Exception in revokeAccess: $error");
    }

    setState(() {
      _state = success
          ? AppState.PERMISSIONS_REVOKED
          : AppState.PERMISSIONS_NOT_REVOKED;
    });
  }

  // UI构建部分省略...

}

此示例展示了如何配置、授权、读取、写入、删除健康数据以及撤销权限。您可以根据需要调整代码以适应具体的应用场景。


更多关于Flutter健康数据访问插件health的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter健康数据访问插件health的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用health插件来访问健康数据的示例代码。这个插件允许你读取和写入来自iOS和Android设备的健康数据。

首先,确保你已经在pubspec.yaml文件中添加了health插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  health: ^0.18.0  # 请检查最新版本号

然后,运行flutter pub get来安装依赖。

1. 配置权限

在iOS上,你需要在Info.plist中添加必要的权限请求。例如,如果你需要访问步数数据,你需要添加:

<key>NSHealthShareUsageDescription</key>
<string>我们需要访问您的健康数据来记录您的步数</string>
<key>NSHealthUpdateUsageDescription</key>
<string>我们需要访问您的健康数据来记录您的步数</string>

在Android上,你需要在AndroidManifest.xml中添加权限请求,但health插件已经为你处理了大部分权限请求。不过,你可能需要在build.gradle文件中启用一些特定的功能,比如:

android {
    ...
    defaultConfig {
        ...
        // 启用对Health API的访问
        applicationId "com.example.yourapp"
        ...
    }
}

2. 请求权限并读取健康数据

以下是一个简单的Flutter应用示例,它请求访问步数数据的权限并读取步数数据:

import 'package:flutter/material.dart';
import 'package:health/health.dart';
import 'package:health/health_types.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> {
  Health? health;
  List<HealthDataPoint<HealthQuantity>>? stepsData;
  bool isLoading = true;
  bool hasPermission = false;

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

  Future<void> _requestHealthData() async {
    health = await Health.init();

    // 请求权限
    bool hasStepsReadPermission = await health?.requestAuthorization(
      [
        HealthDataType.steps,
      ],
    );

    if (hasStepsReadPermission == true) {
      setState(() {
        hasPermission = true;
      });

      // 读取步数数据
      stepsData = await health?.query({
        HealthDataType.steps: QuerySpecification(
          startDate: DateTime.now().subtract(Duration(days: 7)),
          endDate: DateTime.now(),
        ),
      });

      setState(() {
        isLoading = false;
      });
    } else {
      setState(() {
        isLoading = false;
        hasPermission = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('健康数据访问'),
      ),
      body: Center(
        child: isLoading
            ? CircularProgressIndicator()
            : hasPermission
                ? Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text('步数数据:'),
                      ...stepsData!.map((dataPoint) {
                        return Text(
                          '${dataPoint.quantity.toDouble().toInt()} 步 on ${dataPoint.startDate.toLocal()}',
                        );
                      }),
                    ],
                  )
                : Text('没有权限访问健康数据'),
      ),
    );
  }
}

3. 运行应用

确保你已经连接了设备或启动了模拟器,然后运行flutter run来启动应用。

这个示例展示了如何请求访问步数数据的权限,并读取过去7天的步数数据。你可以根据需要修改数据类型和查询规格来访问其他类型的健康数据。

请注意,health插件的API可能会随着版本的更新而变化,因此请查阅最新的官方文档以获取最新的使用方法和最佳实践。

回到顶部