holding

holding 是面向 HarmonyOS/OpenHarmony Flutter 应用的握持手感知插件。插件通过 @kit.MultimodalAwarenessKit 的 motion 能力监听 holdingHandChanged 事件,并向 Dart 层提供未握持、左手、右手、双手和未识别五种状态。

目录

功能特性

  • 订阅和取消订阅系统握持手状态变化。
  • 将鸿蒙系统状态码转换为类型安全的 HoldingHandStatus 枚举。
  • 提供 success、fail、complete 操作回调和持续的 onChange 状态回调。
  • 使用 MethodChannel 调用原生能力,使用 EventChannel 持续传递状态事件。
  • 原生监听重复订阅保护,并在取消监听或 Flutter 引擎解绑时释放资源。
  • 提供可直接运行的示例应用、单元测试和完整的 OHOS 原生实现。

平台与版本

当前 main 分支

项目 当前配置 说明
插件版本 0.0.2 pubspec.yaml 中的当前包版本
Flutter 3.47.9-ohos-1.0.0 oh-3.47-dev 分支
Dart 3.13.4 随上述 Flutter SDK 提供
HarmonyOS 产品版本 7.0.0 (API 26) 当前适配目标
compileSdkVersion 26.0.0 使用 API 26 SDK 编译
targetSdkVersion 26.0.0 面向 API 26 行为构建
compatibleSdkVersion 26.0.0 最低兼容 API 26(随 @ohos/flutter_ohos 最低要求)
支持设备类型 default、tablet 示例 entry 当前配置为 phone

HarmonyOS 7.0.0 对应 API 26。新版 DevEco/Hvigor 的工程配置使用 26.0.0 作为 compileSdkVersion 和 targetSdkVersion,不要在这两个字段中填写旧格式 7.0.0(26)。

当前只实现 ohos 平台。Android、iOS、macOS、Windows、Linux 和 Web 没有平台实现。 此外,运行设备必须提供 holdingHandChanged 系统能力;即使系统 API 版本满足要求,设备硬件或系统产品未开放该能力时也可能无法产生状态事件。

pub.dev 上的 0.0.2 已包含 Flutter 3.47.9/API 26 构建配置,推荐直接使用 Pub 依赖;需要验证 main 分支最新改动时,再使用 Git 依赖。

安装

Pub 依赖

dependencies:
  holding: ^0.0.1

Git 依赖

当前需要使用 Flutter 3.44.9/API 26 配置时,推荐直接依赖仓库 main:

dependencies:
  holding:
    git:
      url: https://atomgit.com/oh-flutter/holding-harmony.git
      ref: main

本地路径依赖

dependencies:
  holding:
    path: ../holding-harmony

添加依赖后执行:

flutter pub get

开发环境

Flutter OHOS SDK

当前仓库使用 Flutter OHOS 的 oh-3.44.9-dev 分支:

git clone https://atomgit.com/CPF-Flutter/flutter_flutter.git \
  -b oh-3.44.9-dev flutter_flutter

export PATH="$(pwd)/flutter_flutter/bin:$PATH"
flutter --version

预期 Flutter 主版本为 3.44.9,并带有 OHOS 版本后缀。CPF-Flutter 与 openharmony-tpc 仓库中该分支当前指向同一提交,也可以使用对应镜像地址。

HarmonyOS/OpenHarmony SDK

在 DevEco Studio 的 SDK Manager 中安装 API 26 的以下组件:

  • ArkTS
  • JS
  • Native
  • Previewer
  • Toolchains

然后配置 Flutter 使用本机 OpenHarmony SDK:

flutter config --ohos-sdk /path/to/OpenHarmony/Sdk
flutter doctor

应用工程的 ohos/build-profile.json5 应包含:

{
  "app": {
    "products": [
      {
        "name": "default",
        "compatibleSdkVersion": "5.1.0(18)",
        "compileSdkVersion": "26.0.0",
        "targetSdkVersion": "26.0.0",
        "runtimeOS": "HarmonyOS"
      }
    ]
  }
}

编译 API 与设备 API 是两个概念。项目使用 API 26 SDK 编译,但 compatibleSdkVersion 为 API 18,因此 API 18 及以上设备满足安装版本要求。例如 API 24 真机可以安装,前提是签名、权限及目标设备系统能力均有效。

应用权限配置

插件 HAR 已声明以下权限:

  • ohos.permission.DETECT_GESTURE
  • ohos.permission.ACTIVITY_MOTION

宿主应用仍应在 entry 模块的 src/main/module.json5 中声明权限及使用场景:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.DETECT_GESTURE",
        "reason": "$string:detect_gesture_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.ACTIVITY_MOTION",
        "reason": "$string:activity_motion_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

同时在 src/main/resources/base/element/string.json 中提供权限说明:

{
  "string": [
    {
      "name": "detect_gesture_reason",
      "value": "用于识别设备握持手状态"
    },
    {
      "name": "activity_motion_reason",
      "value": "用于获取活动与运动感知状态"
    }
  ]
}

具体权限等级、授权方式及上架要求以目标 HarmonyOS 版本的官方文档为准。

快速开始

import 'package:flutter/material.dart';
import 'package:holding/holding.dart';

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

  @override
  State<HoldingPage> createState() => _HoldingPageState();
}

class _HoldingPageState extends State<HoldingPage> {
  final Holding _holding = Holding();
  HoldingHandStatus _status = HoldingHandStatus.none;
  bool _subscribed = false;

  void _subscribe() {
    _holding.subscribeHoldingHand(
      SubscribeHoldingHandOptions(
        onChange: (HoldingHandStatus status) {
          if (!mounted) return;
          setState(() => _status = status);
        },
        success: () {
          if (!mounted) return;
          setState(() => _subscribed = true);
        },
        fail: (String message) {
          if (!mounted) return;
          setState(() => _subscribed = false);
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('订阅失败: $message')),
          );
        },
        complete: (String message) {
          debugPrint('订阅完成: $message');
        },
      ),
    );
  }

  void _unsubscribe() {
    _holding.unsubscribeHoldingHand(
      UnsubscribeHoldingHandOptions(
        success: () {
          if (!mounted) return;
          setState(() => _subscribed = false);
        },
        fail: (String message) {
          debugPrint('取消订阅失败: $message');
        },
      ),
    );
  }

  @override
  void dispose() {
    _holding.unsubscribeHoldingHand();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('握持手状态')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('状态: ${_status.name} (${_status.code})'),
            const SizedBox(height: 12),
            FilledButton(
              onPressed: _subscribed ? _unsubscribe : _subscribe,
              child: Text(_subscribed ? '取消订阅' : '开始订阅'),
            ),
          ],
        ),
      ),
    );
  }
}

页面不再需要感知能力时应调用 unsubscribeHoldingHand。建议在业务层集中管理一个订阅, 并在页面 dispose、功能关闭或账号退出等生命周期节点释放监听。

API 参考

Holding

方法 返回值 说明
subscribeHoldingHand(options) void 建立事件流监听,并通知原生侧订阅握持手状态
unsubscribeHoldingHand([options]) void 通知原生侧停止监听,成功后取消 Dart 事件流订阅
subscribeOperatingHand(options) void 建立事件流监听,并通知原生侧订阅操作手状态
unsubscribeOperatingHand([options]) void 通知原生侧停止操作手监听,成功后取消 Dart 事件流订阅
getRecentOperatingHandStatus(options) void 查询最近一次操作手状态,通过 onSuccess 回调返回

这些方法通过回调异步返回操作结果,因此方法本身返回 void。

SubscribeHoldingHandOptions

属性 类型 必填 触发时机
onChange void Function(HoldingHandStatus) 是 系统握持手状态变化时,可触发多次
success void Function()? 否 原生订阅操作成功时触发一次
fail void Function(String)? 否 MethodChannel 调用或 EventChannel 发生错误时触发
complete void Function(String)? 否 操作成功或失败后触发,参数为结果消息

UnsubscribeHoldingHandOptions

UnsubscribeHoldingHandOptions 是 SubscribeOptionsBase 的类型别名,包含可选的 success、fail、complete 回调。取消订阅参数可以省略:

Holding().unsubscribeHoldingHand();

UnsubscribeOperatingHandOptions 与其相同,用于取消操作手订阅。

SubscribeOperatingHandOptions

属性 类型 必填 触发时机
onChange void Function(OperatingHandStatus) 是 系统操作手状态变化时,可触发多次
success / fail / complete 同 SubscribeHoldingHandOptions 否 订阅操作的结果反馈

GetRecentOperatingHandStatusOptions

属性 类型 必填 触发时机
onSuccess void Function(OperatingHandStatus) 是 查询成功时触发一次,携带最近操作手状态
fail void Function(String)? 否 查询出错时触发
complete void Function(String)? 否 查询成功或失败后触发

HoldingHandStatus

枚举值 系统码 说明
HoldingHandStatus.none 0 未握持
HoldingHandStatus.left 1 左手握持
HoldingHandStatus.right 2 右手握持
HoldingHandStatus.both 3 双手握持
HoldingHandStatus.unknown 16 未识别

HoldingHandStatus.code 可读取原始整数值;HoldingHandStatus.fromCode(int) 可将系统码 转换为枚举。未定义的整数值以及非整数事件都会按 unknown 处理。

OperatingHandStatus

枚举值 系统码 说明
OperatingHandStatus.unknown 0 未识别
OperatingHandStatus.left 1 左手操作
OperatingHandStatus.right 2 右手操作

对应鸿蒙 motion.OperatingHandStatus(since API 15)。code 与 fromCode(int) 的行为 与 HoldingHandStatus 一致,未定义整数值按 unknown 处理。

回调与订阅行为

  • Dart 侧会先建立 EventChannel 监听,再通过 MethodChannel 调用原生订阅,减少首个事件丢失的可能。
  • 同一平台实例只维护一个 EventChannel 订阅。再次调用 subscribeHoldingHand 不会创建第二条原生监听。
  • 重复订阅会更新当前 SubscribeHoldingHandOptions,后续事件交给最近一次传入的 onChange。
  • success 表示订阅或取消订阅操作完成,不表示设备已经产生握持手状态事件。
  • 取消订阅成功后,Dart 侧会取消事件流并清理当前回调;取消失败时会保留当前监听状态。
  • Flutter 引擎解绑或 EventChannel 被取消时,OHOS 侧会调用 motion.off 释放系统监听。

由于插件当前采用单例平台实现,不建议多个页面分别维护并竞争订阅。复杂应用可在状态管理层或服务层统一订阅,再将状态分发给多个页面。

工作原理

Flutter 页面
    |
    | subscribeHoldingHand / unsubscribeHoldingHand
    v
Holding -> HoldingPlatform -> MethodChannel("holding")
                                  |
                                  v
                         HoldingPlugin.ets
                                  |
                                  | motion.on / motion.off
                                  v
                    MultimodalAwarenessKit
                                  |
                                  | HoldingHandStatus 整数状态
                                  v
EventChannel("holding/events") -> Dart 枚举 -> onChange

通道约定:

类型 名称 内容
MethodChannel holding subscribeHoldingHand、unsubscribeHoldingHand、subscribeOperatingHand、unsubscribeOperatingHand、getRecentOperatingHandStatus
EventChannel holding/events 鸿蒙 motion.HoldingHandStatus 整数值
EventChannel holding/operating/events 鸿蒙 motion.OperatingHandStatus 整数值

运行示例

git clone https://atomgit.com/oh-flutter/holding-harmony.git
cd holding-harmony

flutter pub get
flutter analyze
flutter test

cd example
flutter pub get
flutter devices
flutter run -d <device-id>

也可以在 DevEco Studio 中单独打开 example/ohos。项目同步完成后配置 entry 的调试签名, 再选择目标设备运行。签名配置通常包含本机绝对路径和凭据,不应提交到公共仓库。

常见问题

Missing SDK components

典型错误:

Missing SDK components. SDK path: ..., missing components:
toolchains,ets,js,native,previewer

这是电脑端 SDK 目录或组件布局问题,与连接手机的 API 版本无关。请检查:

  1. DevEco SDK Manager 中 API 26 的 ArkTS、JS、Native、Previewer、Toolchains 是否全部安装。
  2. Hvigor 使用的 SDK 根目录是否能够找到同一 API 版本的 openharmony 和 hms 组件。
  3. 修改 SDK 路径后是否完整退出 DevEco Studio,并停止旧 Hvigor daemon 后重新同步。
  4. compileSdkVersion 是否使用当前 Hvigor 接受的 26.0.0 格式。

同步失败时 DevEco 可能暂时不显示 entry 模块,签名页面也无法选择模块。先修复 SDK 并重新同步, 不要把该现象误判为 entry 目录被删除。

手机 API 不是 26,能否运行

可以,只要设备 API 不低于 compatibleSdkVersion。本项目最低兼容 API 18,因此 API 24 设备满足版本条件。compileSdkVersion 只决定电脑使用哪版 SDK 编译,不要求手机必须是相同 API。 最终能否正常产生握持事件还取决于设备是否支持该系统能力。

无法安装未签名 HAP

真机安装需要有效的 HarmonyOS 调试或发布签名。先确保项目同步成功,再在 DevEco Studio 的 File > Project Structure > Signing Configs 中为 entry 配置签名。不要提交证书私钥、口令或 指向个人目录的签名配置。

订阅成功但没有状态事件

依次检查:

  1. entry 是否声明 DETECT_GESTURE 和 ACTIVITY_MOTION 权限。
  2. 应用权限是否已授权,设备是否支持握持手感知能力。
  3. 页面是否仍处于订阅状态,是否被其他页面重复订阅替换了 onChange。
  4. 使用 hilog 检查 HoldingPlugin 是否收到 holdingHandChanged 回调。

取消后仍收到回调

确认业务生命周期中确实调用了 unsubscribeHoldingHand,并检查其 fail 回调。只有原生取消成功后, Dart 侧才会取消 EventChannel 订阅。还应避免多个页面分别订阅同一个平台实例。

其他平台抛出 UnimplementedError

这是预期行为。当前插件仅声明 ohos 平台,其他平台没有实现 subscribeHoldingHand 和 unsubscribeHoldingHand。

项目结构

holding-harmony/
├── lib/
│   ├── holding.dart                     # 对外入口
│   ├── holding_hand_status.dart         # 状态枚举
│   ├── holding_platform_interface.dart  # 平台接口与回调类型
│   └── holding_method_channel.dart      # MethodChannel/EventChannel 实现
├── ohos/
│   ├── index.ets                        # OHOS 插件导出入口
│   └── src/main/
│       ├── ets/components/plugin/HoldingPlugin.ets
│       └── module.json5                 # HAR 模块与权限声明
├── example/                             # 示例 Flutter 应用及 OHOS entry 工程
├── test/                                # Dart 单元测试
└── doc/                                 # Flutter OHOS 插件适配文档

开发文档

参与开发

flutter pub get
flutter analyze
flutter test

cd example
flutter pub get
flutter test

提交问题时建议附上 Flutter 版本、DevEco Studio 版本、SDK API、设备系统版本、完整错误日志和最小复现步骤。

License

本项目基于 Apache License 2.0 开源。