Flutter 三方库 智感握姿 的鸿蒙化适配指南
AI工具 码道 推荐: https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths
欢迎加入 CPF-Flutter 鸿蒙社区: https://atomgit.com/CPF-Flutter
本文以
holding插件的真实适配工程为基础,完整讲解如何把 HarmonyOS 的智感握姿能力封装成 Flutter 插件,并在 Flutter3.44.9、HarmonyOS7.0.0(API 26)环境中完成开发、构建、签名和真机验证。
一、最终运行效果
完成适配后,Flutter 应用能够持续接收系统返回的握持状态,并区分 左手握持、右手握持、双手握持、未握持和未识别五种结果。
下面三张图片均来自 HarmonyOS 真机实际运行。
| 左手握持 | 双手握持 | 右手握持 |
|---|---|---|
系统回调状态码 1 | 系统回调状态码 3 | 系统回调状态码 2 |
说明: 智感握姿依赖系统服务、设备硬件和产品能力。系统版本满足要求,并不必然代表所有型号都能返回握持事件;最终应以目标真机的实际能力为准。
二、什么是智感握姿
智感握姿是 HarmonyOS 多模态感知能力的一部分。应用订阅 holdingHandChanged 事件后,系统会根据设备传感器和系统算法给出当前握持状态。业务层不需要自己读取原始传感器数据,也不需要自行实现左右手识别算法。
典型应用场景包括:
- 阅读器根据左右手动态调整翻页热区;
- 相机把高频操作按钮移动到当前握持手一侧;
- 视频播放器根据单手或双手状态优化控制区域;
- 大屏设备根据握持方式切换紧凑布局或双手布局;
- 无障碍场景中减少跨屏操作距离;
- 游戏根据握持状态改变虚拟按键位置。
这个能力最适合用“事件订阅”建模。应用发起一次订阅,原生侧持续监听系统事件,每次状态变化时再把结果推送给 Dart,而不是让 Flutter 使用定时器轮询原生接口。
三、适配目标与版本矩阵
本文对应仓库当前 main 分支,核心版本如下。
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.44.9+ohos-0.0.1-canary1 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.44.9-dev | CPF-Flutter 对应开发分支 |
| Dart SDK | 3.12.2 | Dart 语言与包管理环境 |
| HarmonyOS | 7.0.0(API 26) | 当前编译和目标系统版本 |
compileSdkVersion | 26.0.0 | 编译时使用的 SDK API |
targetSdkVersion | 26.0.0 | 应用面向的行为版本 |
compatibleSdkVersion | 5.1.0(18) | 当前工程声明的最低兼容版本 |
| 插件版本 | 0.0.1 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | HAR | 被应用 entry 模块依赖 |
3.1 7.0.0(26) 和 26.0.0 为什么不同
这两个写法描述的是相关但不同的概念:
7.0.0(API 26)是面向开发者和用户表达的 HarmonyOS 产品版本与 API 对应关系;26.0.0是新版 DevEco Studio/Hvigor 工程配置中compileSdkVersion、targetSdkVersion使用的格式;5.1.0(18)是当前项目compatibleSdkVersion使用的兼容版本格式。
因此,工程中应该这样写:
{
"name": "default",
"compatibleSdkVersion": "5.1.0(18)",
"compileSdkVersion": "26.0.0",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}
不要因为手机显示的是 API 24,就把本机 API 26 SDK 删除或把所有配置改成 24。编译 SDK 决定构建时可使用哪些 API,兼容 SDK 决定允许安装的最低系统版本,设备 API 则是运行时环境,三者职责不同。
四、整体适配架构
Flutter 侧和 HarmonyOS 侧之间使用两条通道:
MethodChannel('holding'):负责发送“订阅”和“取消订阅”命令;EventChannel('holding/events'):负责持续把握持状态从 ArkTS 推送到 Dart。
这种拆分有三个直接收益:
- 命令调用有明确的成功或失败结果;
- 状态变化可以长期、异步地传输;
- Dart 层可以用平台接口替换真实实现,方便单元测试和后续扩展。
4.1 一次完整订阅的时序
五、工程目录与职责
适配后的关键目录如下:
holding-harmony/
├── lib/
│ ├── holding.dart
│ ├── holding_hand_status.dart
│ ├── holding_method_channel.dart
│ └── holding_platform_interface.dart
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ └── src/main/
│ ├── ets/components/plugin/HoldingPlugin.ets
│ ├── module.json5
│ └── resources/base/element/string.json
├── example/
│ ├── lib/main.dart
│ └── ohos/entry/
├── test/
├── doc/
└── pubspec.yaml
| 文件 | 主要职责 |
|---|---|
lib/holding.dart | 为业务应用提供最简入口 |
lib/holding_hand_status.dart | 声明跨端统一的状态枚举和状态码转换 |
lib/holding_platform_interface.dart | 定义平台无关接口、参数和回调 |
lib/holding_method_channel.dart | 实现 MethodChannel/EventChannel 通信 |
HoldingPlugin.ets | 注册 Flutter 通道并调用 HarmonyOS 原生能力 |
插件 module.json5 | 声明 HAR 模块及权限 |
示例 entry module.json5 | 声明宿主应用 Ability、设备类型和权限场景 |
example/lib/main.dart | 展示订阅、取消订阅、状态显示和事件日志 |
六、准备 Flutter OHOS 3.44.9 环境
6.1 获取 CPF-Flutter SDK
Flutter 的普通官方 SDK 不包含 ohos 平台工具链。应使用 CPF-Flutter 提供的 OHOS 分支:
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
预期输出的重点不是只有 3.44.9,还应看到 OHOS 版本后缀,例如:
Flutter 3.44.9+ohos-0.0.1-canary1
Dart 3.12.2
检查要点: 如果终端仍显示
3.41.x,通常是PATH中旧 Flutter 的优先级更高。执行which -a flutter查看所有命令来源,再把新 SDK 的bin放到PATH最前面。IDE 中还需要同步修改 Flutter SDK 路径,重启 IDE 和终端后再验证。
6.2 安装 HarmonyOS API 26 SDK 组件
在 DevEco Studio 的 SDK Manager 中安装 API 26 对应组件。一个可用于完整 Flutter/HarmonyOS 构建的 SDK 至少应包含:
- ArkTS/ETS;
- JS;
- Native;
- Previewer;
- Toolchains。
然后告诉 Flutter SDK 实际位置:
flutter config --ohos-sdk /path/to/OpenHarmony/Sdk
flutter doctor -v
这里的路径必须指向真正安装了各版本组件的 SDK 根目录,不能仅指向 DevEco Studio 应用包内一个不完整的占位目录。
6.3 环境自检
which flutter
flutter --version
flutter doctor -v
hdc list targets
建议达到以下状态后再开始插件构建:
-
flutter --version是3.44.9OHOS 分支; - Dart 是
3.12.2; -
flutter doctor -v能定位 HarmonyOS/OpenHarmony SDK; - SDK 中存在 API 26 所需组件;
-
hdc list targets能看到已连接真机; - 手机已允许 USB 调试并完成电脑授权。
七、添加插件依赖
当前仓库源码可以直接通过 AtomGit 引入:
dependencies:
flutter:
sdk: flutter
holding:
git:
url: https://atomgit.com/oh-flutter/holding-harmony.git
ref: main
本地联调时使用路径依赖更高效:
dependencies:
holding:
path: ../holding-harmony
然后获取依赖:
flutter pub get
pubspec.yaml中的flutter: ">=3.44.9"是最低 Flutter 约束。业务工程还需要实际选中带 OHOS 支持的 Flutter SDK,仅满足版本数字并不能自动获得ohos平台能力。
八、Dart 层状态模型
原生 API 返回整数状态码。为了避免业务页面到处出现难以理解的数字,插件把状态统一映射为枚举:
enum HoldingHandStatus {
none(0),
left(1),
right(2),
both(3),
unknown(16);
const HoldingHandStatus(this.code);
final int code;
static HoldingHandStatus fromCode(int code) {
return HoldingHandStatus.values.firstWhere(
(item) => item.code == code,
orElse: () => HoldingHandStatus.unknown,
);
}
}
| Dart 枚举 | 状态码 | 业务含义 |
|---|---|---|
HoldingHandStatus.none | 0 | 当前未识别到握持 |
HoldingHandStatus.left | 1 | 左手握持 |
HoldingHandStatus.right | 2 | 右手握持 |
HoldingHandStatus.both | 3 | 双手握持 |
HoldingHandStatus.unknown | 16 | 系统无法识别,或收到未知值 |
fromCode 中必须保留兜底逻辑。以后系统新增状态码时,旧版本插件至少会得到 unknown,不会因为枚举解析失败而让事件流抛出异常。
九、设计平台无关接口
插件没有把 MethodChannel 直接暴露给业务层,而是定义了 HoldingPlatform。这样,真实的 HarmonyOS 通道实现、测试替身以及未来可能增加的平台实现都遵守同一份契约。
typedef HoldingHandChangeCallback = void Function(HoldingHandStatus status);
class SubscribeOptionsBase {
const SubscribeOptionsBase({this.success, this.fail, this.complete});
final void Function()? success;
final void Function(String errMsg)? fail;
final void Function(String errMsg)? complete;
}
class SubscribeHoldingHandOptions extends SubscribeOptionsBase {
const SubscribeHoldingHandOptions({
required this.onChange,
super.success,
super.fail,
super.complete,
});
final HoldingHandChangeCallback onChange;
}
typedef UnsubscribeHoldingHandOptions = SubscribeOptionsBase;
回调的语义需要特别区分:
onChange:状态每次变化时调用,可以执行多次;success:原生订阅或取消订阅命令成功时调用一次;fail:命令失败或事件流报错时调用;complete:一次命令结束后调用,成功和失败都会触发。
对外入口保持简洁:
class Holding {
void subscribeHoldingHand(SubscribeHoldingHandOptions options) {
HoldingPlatform.instance.subscribeHoldingHand(options);
}
void unsubscribeHoldingHand([UnsubscribeHoldingHandOptions? options]) {
HoldingPlatform.instance.unsubscribeHoldingHand(options);
}
}
十、Dart 通道实现
10.1 通道名称必须两端完全一致
final methodChannel = const MethodChannel('holding');
final holdingEventChannel = const EventChannel('holding/events');
通道名称属于跨语言协议。Dart 和 ArkTS 任何一端拼写不一致,都会出现“方法未实现”或“收不到事件”等问题。
10.2 订阅实现
StreamSubscription<dynamic>? _holdingEventSubscription;
SubscribeHoldingHandOptions? _currentHoldingOptions;
void subscribeHoldingHand(SubscribeHoldingHandOptions options) {
_currentHoldingOptions = options;
_holdingEventSubscription ??= holdingEventChannel
.receiveBroadcastStream()
.listen(
(dynamic event) {
final status = event is int
? HoldingHandStatus.fromCode(event)
: HoldingHandStatus.unknown;
_currentHoldingOptions?.onChange(status);
},
onError: (Object error, StackTrace? stackTrace) {
_notifyFail(
options,
_errorMessage(error, 'subscribeHoldingHand:fail'),
);
},
);
methodChannel
.invokeMethod<void>('subscribeHoldingHand')
.then((_) => _notifySuccess(options, 'subscribeHoldingHand:ok'))
.catchError((Object error, StackTrace stackTrace) {
_notifyFail(
options,
_errorMessage(error, 'subscribeHoldingHand:fail'),
);
});
}
这里的关键点有四个:
- 先保存最新
options,状态变化会回调到最新的业务监听者; - 使用
??=避免 Dart 端反复创建 EventChannel 订阅; - 对非整数事件统一转换成
unknown; - MethodChannel 只确认原生监听是否启动成功,真实状态由 EventChannel 返回。
10.3 取消订阅
void unsubscribeHoldingHand([UnsubscribeHoldingHandOptions? options]) {
methodChannel
.invokeMethod<void>('unsubscribeHoldingHand')
.then((_) {
_holdingEventSubscription?.cancel();
_holdingEventSubscription = null;
_currentHoldingOptions = null;
_notifySuccess(options, 'unsubscribeHoldingHand:ok');
})
.catchError((Object error, StackTrace stackTrace) {
_notifyFail(
options,
_errorMessage(error, 'unsubscribeHoldingHand:fail'),
);
});
}
只有原生取消成功后,Dart 层才清理事件订阅和回调引用。这样失败时仍有机会获知原生侧的真实状态,而不是提前丢失所有上下文。
十一、ArkTS 原生插件实现
原生插件位于:
ohos/src/main/ets/components/plugin/HoldingPlugin.ets
11.1 引入 Flutter 和 HarmonyOS 能力
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
EventChannel,
EventSink,
} from '@ohos/flutter_ohos';
import { motion } from '@kit.MultimodalAwarenessKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
其中:
FlutterPlugin负责接入 Flutter Engine 生命周期;MethodChannel接收 Dart 发来的命令;EventChannel和EventSink向 Dart 推送连续事件;motion提供holdingHandChanged系统能力;BusinessError用于读取 HarmonyOS 异常码和异常消息;hilog用于原生侧诊断日志。
11.2 连接 Flutter Engine
private channel: MethodChannel | null = null;
private holdingEventChannel: EventChannel | null = null;
private holdingEventSink: EventSink | null = null;
private holdingCallback: ((data: motion.HoldingHandStatus) => void) | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(
binding.getBinaryMessenger(),
'holding'
);
this.channel.setMethodCallHandler(this);
this.holdingEventChannel = new EventChannel(
binding.getBinaryMessenger(),
'holding/events'
);
this.holdingEventChannel.setStreamHandler({
onListen: (_args: Object, eventSink: EventSink): void => {
this.holdingEventSink = eventSink;
},
onCancel: (_args: Object): void => {
this.stopHoldingListening();
this.holdingEventSink = null;
}
});
}
onListen 表示 Dart 已开始监听事件流,此时保存 EventSink。onCancel 表示 Dart 事件流已经取消,原生侧也应立即停止系统监听,防止页面退出后继续消耗资源。
11.3 订阅系统握持事件
private startHoldingListening(): void {
if (this.holdingCallback != null) return;
this.holdingCallback = (data: motion.HoldingHandStatus) => {
hilog.info(0, TAG, `握持手状态变化: ${data}`);
if (this.holdingEventSink != null) {
this.holdingEventSink.success(data as number);
}
};
motion.on('holdingHandChanged', this.holdingCallback);
hilog.info(0, TAG, '订阅握持手状态成功');
}
第一行幂等判断很重要。Flutter 页面可能因为重复点击、状态恢复或组件重建再次调用订阅。如果每次都注册新的系统回调,同一个状态就可能被发送多次,并造成监听器泄漏。
11.4 取消系统监听
private stopHoldingListening(): void {
if (this.holdingCallback == null) return;
motion.off('holdingHandChanged');
this.holdingCallback = null;
hilog.info(0, TAG, '取消订阅握持手状态成功');
}
调用 motion.off('holdingHandChanged') 后还需要把 holdingCallback 设为 null,否则下一次订阅会被幂等保护误判为“已经订阅”。
11.5 处理 MethodChannel 命令
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case 'subscribeHoldingHand':
this.safeInvoke(
result,
'订阅握持手状态',
() => this.startHoldingListening()
);
break;
case 'unsubscribeHoldingHand':
this.safeInvoke(
result,
'取消订阅握持手状态',
() => this.stopHoldingListening()
);
break;
default:
result.notImplemented();
break;
}
}
safeInvoke 对异常做统一处理:
private safeInvoke(
result: MethodResult,
label: string,
action: () => void
): void {
try {
action();
result.success(null);
} catch (err) {
const error = err as BusinessError;
hilog.error(
0,
TAG,
`${label}失败: code=${error.code}, message=${error.message}`
);
result.error(
String(error.code ?? -1),
error.message ?? `${label}:fail`,
null
);
}
}
异常不能只写日志。通过 result.error 返回 Dart 后,应用才能进入 fail 和 complete 回调,向用户展示可理解的失败状态。
11.6 Engine 解绑时释放资源
onDetachedFromEngine(_binding: FlutterPluginBinding): void {
this.stopHoldingListening();
this.holdingEventSink = null;
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
this.channel = null;
}
if (this.holdingEventChannel != null) {
this.holdingEventChannel.setStreamHandler(null);
this.holdingEventChannel = null;
}
}
插件不能假设业务一定会主动取消订阅。Flutter Engine 被销毁时,原生监听、MethodChannel Handler、EventChannel Handler 和 EventSink 都应释放。
十二、声明插件与宿主应用权限
智感握姿使用了多模态感知能力,当前工程声明了以下权限:
ohos.permission.DETECT_GESTURE
ohos.permission.ACTIVITY_MOTION
12.1 插件 HAR 的权限
在插件的 ohos/src/main/module.json5 中声明:
{
"module": {
"name": "holding",
"type": "har",
"deviceTypes": ["default", "tablet"],
"requestPermissions": [
{
"name": "ohos.permission.DETECT_GESTURE",
"reason": "$string:detect_gesture_reason"
},
{
"name": "ohos.permission.ACTIVITY_MOTION",
"reason": "$string:activity_motion_reason"
}
]
}
}
12.2 应用 entry 的权限
最终安装的是宿主应用,因此业务工程的 ohos/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"
}
}
]
}
}
权限原因资源写入 ohos/entry/src/main/resources/base/element/string.json:
{
"string": [
{
"name": "detect_gesture_reason",
"value": "用于识别设备握持手状态"
},
{
"name": "activity_motion_reason",
"value": "用于获取活动与运动感知状态"
}
]
}
权限是否需要动态申请、是否受系统应用级别限制,以及应用上架时需要哪些说明,应以目标 HarmonyOS 版本的官方权限文档和应用市场审核规则为准。
十三、配置 HarmonyOS 7.0.0(API 26)
业务工程的 ohos/build-profile.json5 应在 products 中设置版本。下面只展示与 SDK 相关的公开配置;签名信息不要复制到文章或提交到公开仓库。
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.1.0(18)",
"runtimeOS": "HarmonyOS",
"compileSdkVersion": "26.0.0",
"targetSdkVersion": "26.0.0"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
十四、在 Flutter 页面中使用
下面是一份完整但保持紧凑的使用示例:
import 'package:flutter/material.dart';
import 'package:holding/holding.dart';
class HoldingPage extends StatefulWidget {
const HoldingPage({super.key});
State<HoldingPage> createState() => _HoldingPageState();
}
class _HoldingPageState extends State<HoldingPage> {
final Holding _holding = Holding();
HoldingHandStatus _status = HoldingHandStatus.none;
bool _subscribed = false;
String get _statusText => switch (_status) {
HoldingHandStatus.none => '未握持',
HoldingHandStatus.left => '左手握持',
HoldingHandStatus.right => '右手握持',
HoldingHandStatus.both => '双手握持',
HoldingHandStatus.unknown => '未识别',
};
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');
},
),
);
}
void dispose() {
_holding.unsubscribeHoldingHand();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('智感握姿')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('当前状态:$_statusText'),
const SizedBox(height: 16),
FilledButton(
onPressed: _subscribed ? null : _subscribe,
child: const Text('订阅'),
),
OutlinedButton(
onPressed: _subscribed ? _unsubscribe : null,
child: const Text('取消订阅'),
),
],
),
),
);
}
}
14.1 页面生命周期注意事项
- 所有异步回调在调用
setState前检查mounted; - 页面销毁时取消订阅,避免继续接收无用事件;
- 按钮根据订阅状态禁用,降低重复操作概率;
fail回调要向用户反馈,不要只打印日志;- 复杂应用建议由服务层集中维护唯一订阅,页面只观察业务状态。
实践建议: 示例在
dispose中直接发起取消命令即可,不要等待异步结果后再调用super.dispose()。生产项目可以让更高层的状态管理服务持有插件实例,从根源上避免页面切换引起反复订阅。
十五、插件自动注册
pubspec.yaml 通过以下配置声明 OHOS 插件类:
flutter:
plugin:
platforms:
ohos:
pluginClass: HoldingPlugin
插件的 ohos/index.ets 需要导出实现:
export { default as HoldingPlugin } from './src/main/ets/components/plugin/HoldingPlugin';
执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。
如果运行时出现 MissingPluginException,依次检查:
- 应用是否使用支持 OHOS 的 Flutter SDK;
pubspec.yaml是否声明了ohos和正确的pluginClass;index.ets是否正确导出插件;oh-package.json5是否能解析插件依赖;- 清理构建缓存后是否重新生成注册文件。
十六、构建、签名与真机运行
16.1 获取依赖与静态检查
flutter clean
flutter pub get
flutter analyze
flutter test
16.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。设备列表为空时,先处理 USB 连接、驱动、调试模式和授权,不要先修改插件代码。
16.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行 Flutter 构建或运行。
安全提醒: 签名口令、私钥文件、证书、Profile 和本机绝对路径都不应提交到公开仓库。CI 应使用密钥管理服务或受保护的环境变量注入签名材料。
16.4 运行示例
cd example
flutter run -d <device-id>
也可以先构建 HAP:
flutter build hap --debug
典型产物位于:
example/ohos/entry/build/default/outputs/default/
目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。
16.5 真机验收动作
- 打开应用,确认初始状态为“未握持”和“未订阅”;
- 点击“订阅”,确认界面进入“已订阅”;
- 分别使用左手、右手和双手自然握持设备;
- 检查界面状态和时间日志是否随握姿变化;
- 点击“取消”,继续改变握姿,确认不再产生新日志;
- 退出页面再进入,确认不会出现重复事件;
- 多次订阅和取消,确认无崩溃、无重复回调。
十七、测试策略
通道插件至少需要覆盖三类测试。
17.1 平台接口测试
确认默认实例是 MethodChannel 实现:
test('MethodChannelHolding is the default instance', () {
expect(
HoldingPlatform.instance,
isInstanceOf<MethodChannelHolding>(),
);
});
17.2 通道协议测试
test('uses the expected platform channels', () {
final platform = MethodChannelHolding();
expect(platform.methodChannel.name, 'holding');
expect(platform.holdingEventChannel.name, 'holding/events');
});
建议继续补充以下用例:
- 状态码
0/1/2/3/16能正确转换; - 未知整数转换为
unknown; - 非整数事件转换为
unknown; - 订阅成功触发
success和complete; - 平台异常触发
fail和complete; - 取消成功后清理 EventChannel 订阅;
- 重复订阅不会创建多个 Dart StreamSubscription。
17.3 Widget 测试
示例工程应至少验证初始 UI:
testWidgets('shows the initial holding state', (tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('Holding 插件示例'), findsOneWidget);
expect(find.text('未握持'), findsOneWidget);
expect(find.text('未订阅'), findsOneWidget);
expect(find.text('订阅'), findsOneWidget);
});
Dart 单元测试无法替代真机测试。
motion的系统算法、硬件支持、权限状态和签名环境只能通过真实 HarmonyOS 设备验证。
十八、常见问题与排查方法
18.1 Missing SDK components
典型错误如下:
Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.
这不是因为手机不是 API 26。报错发生在 Hvigor 同步阶段,说明构建工具当前找到的 SDK 根目录缺少组件,甚至还没有进入设备安装阶段。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录; - 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 Sync 和 Compile 首先读取 Mac 本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 API 18,因此 API 24 在安装版本门槛上是满足的;但设备还必须支持智感握姿能力,并满足签名和权限要求。
18.2 DevEco Studio 中看不到 entry 模块
插件根目录自身是 HAR 模块,不是可安装应用,所以不会天然拥有业务应用的 entry。本仓库的可运行模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
holding-harmony/example/ohos
如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
18.3 无法手动签名
签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。
建议先确认:
- 打开的是
example/ohos; - SDK 组件完整并且 Sync 成功;
entry的模块类型为entry;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效。
18.4 能安装但没有状态变化
按以下顺序检查:
- UI 是否已经显示“已订阅”;
- entry 是否声明
DETECT_GESTURE和ACTIVITY_MOTION; - 系统是否授予相关权限;
hilog中是否出现“订阅握持手状态成功”;hilog中是否出现“握持手状态变化”;- 目标设备型号和系统是否真正支持
holdingHandChanged; - 是否使用过厚保护壳或不符合设备识别条件的握持方式。
18.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件,而不是 motion API 自身失败。清理并重新生成:
flutter clean
flutter pub get
cd example
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件中是否包含 HoldingPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。
18.6 重复收到相同事件
重点检查两处幂等保护:
- Dart 侧使用
_holdingEventSubscription ??=; - ArkTS 侧在
holdingCallback != null时直接返回。
还应确认业务层没有同时创建多个 Holding 服务实例并分别订阅。如果多个页面都需要握持状态,推荐在应用级服务中只订阅一次,再通过状态管理分发。
18.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
先保留完整安装错误码,再判断是签名、版本还是包冲突,不建议只反复执行 flutter clean。
十九、生产环境设计建议
示例工程证明能力可用后,正式项目还应考虑稳定性、体验和可观测性。
19.1 集中管理订阅
在应用级服务中持有唯一 Holding 实例,把原始枚举转换为业务状态,再由 Provider、Riverpod、Bloc 或项目现有状态管理方案分发。这样可以避免多个页面同时注册系统监听。
19.2 防抖与状态稳定
传感器结果可能在短时间内快速切换。业务如果立即重排整个页面,会造成视觉抖动。可以根据场景增加 100~300 ms 的防抖或连续多次一致后再确认,但不要在插件底层强制写死阈值,因为阅读器、相机和游戏对延迟的容忍度不同。
19.3 能力降级
把智感握姿视为增强能力,而不是关键路径的唯一入口:
- 设备不支持时保留普通操作布局;
- 权限拒绝时提供手动左右手设置;
- 收到
unknown时维持上一个稳定状态或回退默认布局; - 订阅失败时允许用户重试,但避免无限循环重试。
19.4 日志与隐私
建议记录订阅成功、失败码、取消订阅和能力不可用等诊断信息,但不要把用户握持历史与身份信息长期关联。发布版本应控制日志级别,并遵守隐私政策和最小化采集原则。
19.5 CI 建议
持续集成至少执行:
flutter pub get
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test
flutter build hap --debug
同时注意:
- 固定 Flutter OHOS SDK 分支或提交,避免构建环境漂移;
- 固定 ohpm/Hvigor 依赖版本;
- 缓存依赖但不要缓存签名秘密;
- 构建日志中屏蔽证书路径和密码;
- 使用专门的真机流水线完成硬件能力回归。
二十、总结
holding 的鸿蒙化适配看似只是调用一次 motion.on,真正需要处理的是一整条跨端链路:
Flutter 业务 API → 平台接口 → MethodChannel 命令 → ArkTS 系统订阅 → EventChannel 事件 → Dart 枚举 → 页面状态
一份可靠的适配不仅要“能收到左手或右手”,还要做到版本配置准确、权限完整、通道协议稳定、重复订阅幂等、生命周期可释放、异常可回传,并在真实设备上覆盖订阅和取消订阅的完整闭环。
本文工程已经完成 Flutter 3.44.9 与 HarmonyOS 7.0.0(API 26) 的适配配置,并通过三种典型握姿进行了真机验证。开发者可以直接参考配套仓库,把智感握姿进一步接入阅读、相机、影音、游戏和大屏交互场景。
相关链接
- CPF-Flutter 鸿蒙社区: https://atomgit.com/CPF-Flutter
- Flutter OHOS SDK: https://atomgit.com/CPF-Flutter/flutter_flutter
- holding 配套仓库: https://atomgit.com/oh-flutter/holding-harmony
- holding Issues: https://atomgit.com/oh-flutter/holding-harmony/issues
- AI工具 码道: https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths
本文代码和配置以配套仓库当前
main分支为准。HarmonyOS SDK、Flutter OHOS 工具链和设备能力会持续演进,升级后请重新执行静态检查、构建测试和真机回归。
更多推荐


所有评论(0)