Flutter 三方库 holding 的 OpenHarmony 适配实战:从 0 到 1 开发一个鸿蒙感知插件

本文记录了从 0 到 1 实现开源 Flutter 三方库 holding 并适配到 OpenHarmony / HarmonyOS 平台的完整过程,包含生态价值分析、适配思路、代码实现、API 26 能力扩展、测试验证、发布流程与踩坑复盘,供其他开发者快速复刻同类插件的鸿蒙化工作。


一、背景

1.1 生态价值:为什么值得做这类插件

HarmonyOS 已经形成独立的 Flutter 生态(Flutter OH),由 CPF-Flutter 提供引擎与框架的鸿蒙分支。生态繁荣的关键在于 pub.dev 上的原生能力类插件能否在鸿蒙上开箱即用——这决定了现有 Flutter 应用迁移到鸿蒙的成本。

holding 插件正是这类"系统能力桥接"插件的典型代表:

  • 能力来自鸿蒙专属 Kit:握持手状态(holdingHandChanged)与操作手状态(operatingHandChanged)由鸿蒙 @kit.MultimodalAwarenessKit 的 motion 模块提供,Android/iOS 生态没有完全等价能力,因此它不是"翻译"某个 Android 插件,而是 把鸿蒙专属系统感知能力暴露给 Flutter 生态;
  • 应用场景真实:单手/双手握持感知可用于适老化交互、单手键盘、双手协同阅读、防误触等特性,是 Mate 系列/折叠屏等设备的差异化交互基础;
  • 方法论可复制:本文的"创建插件工程 → Dart 层接口 → ArkTS 原生实现 → 通道注册 → 示例验证 → API 26 扩展 → 发布"的完整路径,适用于所有基于鸿蒙 Kit 的感知类插件(屏幕状态、设备状态、用户状态等),同类工作可直接对照。

1.2 三方库简介

holding 是一个 Flutter 握持手感知插件,仓库地址为 atomgit.com/oh-flutter/holding-harmony,提供以下能力:

  • 握持手状态订阅:subscribeHoldingHand / unsubscribeHoldingHand,监听设备被哪只手握持(未握持/左手/右手/双手/未识别五种状态);
  • 操作手状态订阅:subscribeOperatingHand / unsubscribeOperatingHand,监听用户正在用哪只手操作设备(左手/右手/未识别);
  • 最近操作手查询:getRecentOperatingHandStatus,同步查询系统缓存的最近一次操作手状态;
  • 类型安全的状态枚举:HoldingHandStatus、OperatingHandStatus,与鸿蒙系统枚举一一对应,提供 code 与 fromCode(int) 转换。

插件当前仅声明 ohos 平台实现,其他平台调用会抛出 UnimplementedError(预期行为)。

项目地址:atomgit.com/oh-flutter/holding-harmony(Issue / PR 均在此仓库提交)
Pub 包:pub.dev/packages/holding

1.3 适配目标

维度要求
功能一致性握持手/操作手的订阅、取消订阅、查询行为与鸿蒙 motion API 一致,错误码通过回调反馈
Dart 层零破坏Dart 接口一次成型,后续 API 26 能力扩展只做增量,不破坏已发布版本的接口契约
性能事件流直通:原生回调 → EventSink → Dart onChange,不做轮询,无中间缓冲
工程规范通道名/方法名/错误消息统一契约;中英双 README(README.OpenHarmony.md / README.OpenHarmony_CN.md)、双 CHANGELOG、真机验证、单元测试齐全
版本对齐与最新鸿蒙 SDK(API 26)和最新 Flutter OH 版本对齐,并纳入 API 26 SDK 相对插件的新增 API

二、适配路线图

整个实现分为 6 个阶段:

第 1 阶段:环境搭建     ── Flutter OH SDK + DevEco Studio + HarmonyOS SDK (API 26)
第 2 阶段:工程创建     ── flutter create 生成 ohos 插件模板
第 3 阶段:Dart 层接口  ── 平台接口 + 枚举 + MethodChannel/EventChannel 实现
第 4 阶段:原生实现     ── ArkTS 插件,桥接 @kit.MultimodalAwarenessKit
第 5 阶段:API 26 扩展  ── 纳入新 SDK 的 operatingHandChanged / getRecentOperatingHandStatus
第 6 阶段:验证与发布   ── 单元测试 + 真机运行 + hvigor 构建 + 发布到 pub.dev

其中第 5 阶段是本次"结合最新鸿蒙 SDK 做升级"的核心:对照 API 26 SDK 的 @ohos.multimodalAwareness.motion.d.ts,把插件尚未覆盖的系统 API 增量纳入插件能力。


三、逐步实现过程

第 1 阶段:环境搭建

本文验证时使用的环境(按版本获取方式可自查):

项目版本获取方式
Flutter3.47.9-ohos-1.0.0(oh-3.47-dev 渠道)flutter --version
Dart3.13.4flutter --version
HarmonyOS SDK26.0.0(API 26)~/Library/OpenHarmony/Sdk/ 目录名,或 DevEco 内 SDK 管理器
IDEDevEco Studio 26.0.0defaults read /Applications/DevEco-Studio.app/Contents/Info.plist CFBundleShortVersionString
真机HUAWEI Mate 60 Pro(ALN-AL00),ROM 7.0.0.111(SP10C00E111R4P3),OpenHarmony API 26hdc list targets + hdc shell param get const.product.software.version
pub 镜像pub.flutter-io.cn(下载用)/ pub.dev(发布用)flutter config 或环境变量 PUB_HOSTED_URL

Flutter OH SDK 来自 atomgit.com/oh-flutter/flutter_flutter 的 oh-3.47-dev 分支:

git clone https://atomgit.com/oh-flutter/flutter_flutter.git \
  -b oh-3.47-dev flutter_flutter

export PATH="$(pwd)/flutter_flutter/bin:$PATH"
flutter --version   # 预期带 -ohos 后缀,且 doctor 显示 HarmonyOS toolchain ✓

API 26 格式注意:新版 DevEco/Hvigor 的工程配置使用 26.0.0 作为 compileSdkVersion / targetSdkVersion / compatibleSdkVersion,不要填写旧格式 7.0.0(26)。

第 2 阶段:工程创建

在目标目录用 Flutter 命令行生成 OHOS 插件模板:

flutter create . --template=plugin --platforms=ohos

该命令自动生成 ohos/ 目录的标准模板结构:

ohos/
├── index.ets                              # 模块入口,导出插件类
├── oh-package.json5                       # 包配置
├── build-profile.json5                    # 构建配置
├── src/main/
│   ├── module.json5                       # HAR 模块配置
│   └── ets/components/plugin/
│       └── HoldingPlugin.ets              # 原生插件实现(核心)

index.ets(入口导出):

import HoldingPlugin from './src/main/ets/components/plugin/HoldingPlugin';
export default HoldingPlugin;

ohos/build-profile.json5(HAR 构建配置,保持模板最小化即可,SDK 级别由宿主工程决定):

{
  "apiType": "stageMode",
  "buildOption": {
  },
  "targets": [
    {
      "name": "default"
    }
  ]
}

注:@ohos/flutter_ohos 由 Flutter 引擎在构建时自动链接,无需在 oh-package.json5 的 dependencies 中显式声明。

第 3 阶段:Dart 层接口

按插件通信机制分类,holding 属于事件流型(原生持续推送状态变化,EventChannel + StreamHandler)叠加方法调用型(订阅/取消/查询操作本身,MethodChannel + MethodCallHandler)。Dart 层采用鸿蒙风格回调(success/fail/complete),接口一次成型:

目录结构:

lib/
├── holding.dart                           # 对外门面类 Holding
├── holding_hand_status.dart               # 握持手枚举(since API 20)
├── operating_hand_status.dart             # 操作手枚举(since API 15)
├── holding_platform_interface.dart        # 平台接口 + 订阅选项类
└── holding_method_channel.dart            # 通道实现(核心)

状态枚举(lib/holding_hand_status.dart,operating_hand_status.dart 同构):

/// 与鸿蒙 motion.HoldingHandStatus 对应
enum HoldingHandStatus {
  none(0),      // 未握持
  left(1),      // 左手握持
  right(2),     // 右手握持
  both(3),      // 双手握持
  unknown(16);  // 未识别

  const HoldingHandStatus(this.code);

  /// 鸿蒙系统返回的整型状态值
  final int code;

  /// 从鸿蒙状态码解析,未定义值映射为 unknown
  static HoldingHandStatus fromCode(int code) {
    return HoldingHandStatus.values.firstWhere(
      (e) => e.code == code,
      orElse: () => HoldingHandStatus.unknown,
    );
  }
}

门面类(lib/holding.dart):

class Holding {
  /// 订阅握持手状态变化
  void subscribeHoldingHand(SubscribeHoldingHandOptions options) {
    HoldingPlatform.instance.subscribeHoldingHand(options);
  }

  /// 取消订阅握持手状态变化
  void unsubscribeHoldingHand([UnsubscribeHoldingHandOptions? options]) {
    HoldingPlatform.instance.unsubscribeHoldingHand(options);
  }

  /// 订阅操作手状态变化(API 26 SDK 扩展能力)
  void subscribeOperatingHand(SubscribeOperatingHandOptions options) {
    HoldingPlatform.instance.subscribeOperatingHand(options);
  }

  /// 取消订阅操作手状态变化
  void unsubscribeOperatingHand([UnsubscribeOperatingHandOptions? options]) {
    HoldingPlatform.instance.unsubscribeOperatingHand(options);
  }

  /// 查询最近一次操作手状态(API 26 SDK 扩展能力)
  void getRecentOperatingHandStatus(
    GetRecentOperatingHandStatusOptions options,
  ) {
    HoldingPlatform.instance.getRecentOperatingHandStatus(options);
  }
}

订阅选项类(lib/holding_platform_interface.dart,与鸿蒙回调风格对齐):

/// 通用的订阅选项基类(success / fail / complete 回调)
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;
}

/// 订阅握持手选项:onChange 必填,持续接收状态
class SubscribeHoldingHandOptions extends SubscribeOptionsBase {
  const SubscribeHoldingHandOptions({
    required this.onChange,
    super.success,
    super.fail,
    super.complete,
  });

  final HoldingHandChangeCallback onChange;
}

/// 取消订阅选项(可整体省略)
typedef UnsubscribeHoldingHandOptions = SubscribeOptionsBase;

/// 查询最近操作手状态选项:onSuccess 必填,携带状态返回
class GetRecentOperatingHandStatusOptions {
  const GetRecentOperatingHandStatusOptions({
    required this.onSuccess,
    this.fail,
    this.complete,
  });

  final void Function(OperatingHandStatus status) onSuccess;
  final void Function(String errMsg)? fail;
  final void Function(String errMsg)? complete;
}

SubscribeOperatingHandOptions / UnsubscribeOperatingHandOptions 与握持手版本同构。

通道实现(lib/holding_method_channel.dart,核心策略:先建事件流监听再发起原生订阅,减少首个事件丢失;同一能力只维护一条事件订阅):

class MethodChannelHolding extends HoldingPlatform {
  
  final methodChannel = const MethodChannel('holding');

  
  final holdingEventChannel = const EventChannel('holding/events');

  
  final operatingEventChannel = const EventChannel('holding/operating/events');

  StreamSubscription<dynamic>? _holdingEventSubscription;
  SubscribeHoldingHandOptions? _currentHoldingOptions;

  StreamSubscription<dynamic>? _operatingEventSubscription;
  SubscribeOperatingHandOptions? _currentOperatingOptions;

  // ---------- 通用工具方法 ----------

  /// 从错误对象中提取消息
  String _errorMessage(Object error, String fallback) {
    return error is PlatformException
        ? error.message ?? fallback
        : error.toString();
  }

  /// 触发 success + complete 回调
  void _notifySuccess(SubscribeOptionsBase? opts, String okMsg) {
    opts?.success?.call();
    opts?.complete?.call(okMsg);
  }

  /// 触发 fail + complete 回调
  void _notifyFail(SubscribeOptionsBase? opts, String errMsg) {
    opts?.fail?.call(errMsg);
    opts?.complete?.call(errMsg);
  }

  // ---------- 握持手 ----------

  
  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'),
          );
        });
  }

  
  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'),
          );
        });
  }

  // ---------- 操作手(API 26 SDK 扩展能力,与握持手同构) ----------

  
  void subscribeOperatingHand(SubscribeOperatingHandOptions options) {
    _currentOperatingOptions = options;

    _operatingEventSubscription ??= operatingEventChannel
        .receiveBroadcastStream()
        .listen(
          (dynamic event) {
            final status = event is int
                ? OperatingHandStatus.fromCode(event)
                : OperatingHandStatus.unknown;
            _currentOperatingOptions?.onChange(status);
          },
          onError: (Object error, StackTrace? stackTrace) {
            _notifyFail(
              options,
              _errorMessage(error, 'subscribeOperatingHand:fail'),
            );
          },
        );

    methodChannel
        .invokeMethod<void>('subscribeOperatingHand')
        .then((_) => _notifySuccess(options, 'subscribeOperatingHand:ok'))
        .catchError((Object error, StackTrace stackTrace) {
          _notifyFail(
            options,
            _errorMessage(error, 'subscribeOperatingHand:fail'),
          );
        });
  }

  
  void unsubscribeOperatingHand([UnsubscribeOperatingHandOptions? options]) {
    methodChannel
        .invokeMethod<void>('unsubscribeOperatingHand')
        .then((_) {
          _operatingEventSubscription?.cancel();
          _operatingEventSubscription = null;
          _currentOperatingOptions = null;
          _notifySuccess(options, 'unsubscribeOperatingHand:ok');
        })
        .catchError((Object error, StackTrace stackTrace) {
          _notifyFail(
            options,
            _errorMessage(error, 'unsubscribeOperatingHand:fail'),
          );
        });
  }

  
  void getRecentOperatingHandStatus(
    GetRecentOperatingHandStatusOptions options,
  ) {
    methodChannel
        .invokeMethod<int>('getRecentOperatingHandStatus')
        .then((code) {
          options.onSuccess(
            code is int
                ? OperatingHandStatus.fromCode(code)
                : OperatingHandStatus.unknown,
          );
          options.complete?.call('getRecentOperatingHandStatus:ok');
        })
        .catchError((Object error, StackTrace stackTrace) {
          final errMsg = _errorMessage(
            error,
            'getRecentOperatingHandStatus:fail',
          );
          options.fail?.call(errMsg);
          options.complete?.call(errMsg);
        });
  }
}

第 4 阶段:原生实现(ArkTS 桥接鸿蒙 Kit)

这是适配的核心工作:在 onAttachedToEngine 里注册一条 MethodChannel 与两条 EventChannel,把 @kit.MultimodalAwarenessKit 的 motion.on / motion.off / motion.getRecentOperatingHandStatus() 桥接到 Flutter 侧。

完整实现(ohos/src/main/ets/components/plugin/HoldingPlugin.ets):

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';

const TAG = 'HoldingPlugin';

export default class HoldingPlugin implements FlutterPlugin, MethodCallHandler {
  private channel: MethodChannel | null = null;
  private holdingEventChannel: EventChannel | null = null;
  private holdingEventSink: EventSink | null = null;
  private holdingCallback: ((data: motion.HoldingHandStatus) => void) | null = null;
  private operatingEventChannel: EventChannel | null = null;
  private operatingEventSink: EventSink | null = null;
  private operatingCallback: ((data: motion.OperatingHandStatus) => void) | null = null;

  getUniqueClassName(): string {
    return "HoldingPlugin"
  }

  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;
      }
    });

    this.operatingEventChannel = new EventChannel(binding.getBinaryMessenger(), "holding/operating/events");
    this.operatingEventChannel.setStreamHandler({
      onListen: (_args: Object, eventSink: EventSink): void => {
        this.operatingEventSink = eventSink;
      },
      onCancel: (_args: Object): void => {
        this.stopOperatingListening();
        this.operatingEventSink = null;
      }
    });
  }

  onDetachedFromEngine(_binding: FlutterPluginBinding): void {
    this.stopHoldingListening();
    this.holdingEventSink = null;
    this.stopOperatingListening();
    this.operatingEventSink = null;
    if (this.channel != null) {
      this.channel.setMethodCallHandler(null);
      this.channel = null;
    }
    if (this.holdingEventChannel != null) {
      this.holdingEventChannel.setStreamHandler(null);
      this.holdingEventChannel = null;
    }
    if (this.operatingEventChannel != null) {
      this.operatingEventChannel.setStreamHandler(null);
      this.operatingEventChannel = null;
    }
  }

  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);
    }
  }

  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, '订阅握持手状态成功');
  }

  private stopHoldingListening(): void {
    if (this.holdingCallback == null) return;
    motion.off('holdingHandChanged');
    this.holdingCallback = null;
    hilog.info(0, TAG, '取消订阅握持手状态成功');
  }

  private startOperatingListening(): void {
    if (this.operatingCallback != null) return;
    this.operatingCallback = (data: motion.OperatingHandStatus) => {
      hilog.info(0, TAG, `操作手状态变化: ${data}`);
      if (this.operatingEventSink != null) {
        this.operatingEventSink.success(data as number);
      }
    };
    motion.on('operatingHandChanged', this.operatingCallback);
    hilog.info(0, TAG, '订阅操作手状态成功');
  }

  private stopOperatingListening(): void {
    if (this.operatingCallback == null) return;
    motion.off('operatingHandChanged');
    this.operatingCallback = null;
    hilog.info(0, TAG, '取消订阅操作手状态成功');
  }

  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;
      case "subscribeOperatingHand":
        this.safeInvoke(result, '订阅操作手状态', () => this.startOperatingListening());
        break;
      case "unsubscribeOperatingHand":
        this.safeInvoke(result, '取消订阅操作手状态', () => this.stopOperatingListening());
        break;
      case "getRecentOperatingHandStatus":
        try {
          const status = motion.getRecentOperatingHandStatus() as number;
          result.success(status);
        } catch (err) {
          const error = err as BusinessError;
          hilog.error(0, TAG, `查询操作手状态失败: code=${error.code}, message=${error.message}`);
          result.error(String(error.code ?? -1), error.message ?? 'getRecentOperatingHandStatus:fail', null);
        }
        break;
      default:
        result.notImplemented();
        break;
    }
  }
}

StreamHandler 生命周期对应关系(事件流型插件的核心契约):

时机插件侧动作
插件绑定引擎onAttachedToEngine 创建 EventChannel + setStreamHandler
Dart 开始订阅onListen → 持有 EventSink(此时原生事件到达即可直推)
事件到达系统回调中 eventSink.success(整型状态) → Dart onChange
Dart 取消订阅onCancel → motion.off 注销系统监听 + 释放 EventSink
插件解绑引擎onDetachedFromEngine → 兜底 motion.off + setStreamHandler(null)

防御点:startXxxListening 幂等(回调非空即 return),重复订阅不会注册第二条系统监听;onCancel 与 onDetachedFromEngine 双路径兜底注销,避免 Flutter 引擎解绑后系统监听泄漏。

第 5 阶段:API 26 SDK 能力扩展(对照官方 .d.ts 做增量升级)

"结合最新鸿蒙 SDK 做升级"不能靠猜——正确做法是 直接对照已安装 SDK 的类型声明做 API 差异分析。打开 API 26 SDK 中的 @ohos.multimodalAwareness.motion.d.ts(位于 <SDK>/ets/api/),与插件已覆盖能力逐项对照:

API 26 SDK 中的 motion APIsince插件适配前适配决策
on/off('operatingHandChanged') + OperatingHandStatus(0/1/2)15❌ 未覆盖✅ 纳入:新增操作手订阅/取消 + Dart 枚举
getRecentOperatingHandStatus()15❌ 未覆盖✅ 纳入:新增查询型 API(同步查询 → MethodChannel 异步回调)
on/off('holdingHandChanged') + HoldingHandStatus(0/1/2/3/16)20✅ 已覆盖保持
on/off('operatingHandChanged') 权限要求 since 20 放宽为 ACTIVITY_MOTION 或 DETECT_GESTURE15→20✅ 两个权限均已声明保持

两个 since 版本要点:

  • holdingHandChanged 是 since 20 API,依赖 ohos.permission.DETECT_GESTURE;
  • operatingHandChanged 与 getRecentOperatingHandStatus 是 since 15 API,since 20 起权限放宽为 ACTIVITY_MOTION 或 DETECT_GESTURE——插件 HAR 同时声明两者,宿主任选其一即可。

API 26 升级带来的版本下限变化(重要决策,详见第五章决策 3):新版 Flutter OH SDK 构建产出的 @ohos/flutter_ohos HAR 要求宿主 compatibleSdkVersion >= 26,因此插件构建配置的最低兼容版本从 5.1.0(18) 上调至 26.0.0。

第 6 阶段:插件注册与示例验证

插件注册(pubspec.yaml):

name: holding
description: Flutter 握持手感知插件,支持 HarmonyOS 与 OpenHarmony,订阅握持手与操作手状态变化,并可查询最近操作手状态。
version: 0.0.2
homepage: https://atomgit.com/oh-flutter/holding-harmony
repository: https://atomgit.com/oh-flutter/holding-harmony
issue_tracker: https://atomgit.com/oh-flutter/holding-harmony/issues
documentation: https://atomgit.com/oh-flutter/holding-harmony
license: Apache-2.0

environment:
  sdk: ^3.12.2
  flutter: ">=3.44.9"

flutter:
  plugin:
    platforms:
      ohos:                              # ← 对应 index.ets 默认导出
        pluginClass: HoldingPlugin

仓库地址统一为 AtomGit:homepage / repository / issue_tracker / documentation 四个元数据字段全部指向 atomgit.com/oh-flutter/holding-harmony,保证 pub.dev 包页面上的链接与代码托管一致。

示例宿主工程(在 example/ 目录而非插件根目录执行):

cd example
flutter create . --platforms=ohos

关键配置(example/ohos/build-profile.json5):

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

权限声明(example/ohos/entry/src/main/module.json5,reason 必须是 $string 资源引用,usedScene 必填):

"requestPermissions": [
  { "name": "ohos.permission.INTERNET" },
  {
    "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" }
  }
]

示例 UI 提供"握持手/操作手"两张状态卡片,各带订阅/取消按钮,操作手额外提供"查询最近操作手状态"按钮,状态变化记录到日志列表,便于真机观察(完整代码见 example/lib/main.dart)。


四、完整代码对照

4.1 通道契约总表

层名称方法 / 内容对应系统 API
MethodChannelholdingsubscribeHoldingHand / unsubscribeHoldingHandmotion.on / motion.off('holdingHandChanged')
MethodChannelholdingsubscribeOperatingHand / unsubscribeOperatingHandmotion.on / motion.off('operatingHandChanged')
MethodChannelholdinggetRecentOperatingHandStatusmotion.getRecentOperatingHandStatus()
EventChannelholding/events鸿蒙 motion.HoldingHandStatus 整数值(0/1/2/3/16)holdingHandChanged 回调参数
EventChannelholding/operating/events鸿蒙 motion.OperatingHandStatus 整数值(0/1/2)operatingHandChanged 回调参数

通道名是 Dart 与原生之间的通信契约,两侧必须完全一致;Dart 层通道名集中在 MethodChannelHolding 的 @visibleForTesting 字段,原生层集中在 HoldingPlugin.onAttachedToEngine,各只有一处定义。

4.2 鸿蒙系统 API 与 Dart 枚举对照

鸿蒙 motion.HoldingHandStatus系统码Dart HoldingHandStatus
NOT_HELD0none
LEFT_HAND_HELD1left
RIGHT_HAND_HELD2right
BOTH_HANDS_HELD3both
UNKNOWN_STATUS16unknown
鸿蒙 motion.OperatingHandStatus系统码Dart OperatingHandStatus
UNKNOWN_STATUS0unknown
LEFT_HAND_OPERATED1left
RIGHT_HAND_OPERATED2right

4.3 关键 ArkTS 语法差异(面向 Android/iOS 插件作者)

Kotlin/Java 习惯ArkTS 写法备注
import io.flutter.embedding.engine.FlutterEngineimport { ... } from '@ohos/flutter_ohos'鸿蒙使用模块化导入,引擎包名固定
MethodChannel(messenger, name)new MethodChannel(binding.getBinaryMessenger(), name)构造参数一致;messenger 从 FlutterPluginBinding 获取
EventChannel.StreamHandlersetStreamHandler({ onListen, onCancel })鸿蒙用对象字面量实现接口,无需单独声明类
eventSink.success(int)this.eventSink?.success(data as number)鸿蒙枚举需 as number 显式转整型
try/catch (e: Exception)catch (err) { const error = err as BusinessError }ArkTS catch 参数无类型标注,需手动断言 BusinessError 取 code/message
系统监听器注册motion.on('xxx', callback)Kit API 直接传回调,无监听器对象

错误码透传约定:原生异常时 result.error(String(error.code ?? -1), error.message ?? '${label}:fail', null),把鸿蒙 BusinessError.code(如 201 权限拒绝、801 能力不支持、31500002 订阅失败)原样透传给 Dart 层 fail 回调,便于宿主应用针对错误码降级。


五、关键决策说明

决策 1:保持鸿蒙风格回调,而非 Future

插件接口采用鸿蒙 Kit 风格的 success / fail / complete 回调(而非 Future<T>),onChange 必填持续接收状态。理由:目标用户是熟悉鸿蒙 API 的开发者,回调签名与 motion.on 的使用心智一致;且事件流是持续推送,Future 只能表达一次性结果,无法承载"订阅"语义。

维护策略:接口一经发布不再变更签名;后续扩展(如操作手查询)复用 success/fail/complete 心智,但查询类用携带状态的 onSuccess(GetRecentOperatingHandStatusOptions),避免与订阅类的无参 success 混淆。

决策 2:先建事件流再发原生订阅

subscribeXxxHand 中 Dart 侧先 receiveBroadcastStream().listen(...) 建立事件流监听,再通过 MethodChannel 通知原生订阅。理由:若原生先订阅、Dart 事件流晚一步建立,首个系统事件可能丢失(握持状态切换很快);??= 保证同一能力只建一条事件订阅,重复订阅只更新 _currentXxxOptions,把后续事件交给最近一次传入的 onChange。

维护策略:新增事件流型能力时复制该模式;取消订阅必须走"原生 off 成功后才 cancel 事件流"的顺序,取消失败保留监听状态。

决策 3:compatibleSdkVersion 从 18 上调至 26

最初构建配置把 compatibleSdkVersion 设为 5.1.0(18)(旧格式,最低兼容 API 18),但 flutter build hap 报错:新版 Flutter OH SDK 产出的 @ohos/flutter_ohos HAR 的最低兼容版本高于 18,宿主必须上调。选择把 compatibleSdkVersion 统一为 26.0.0(新格式,与 compile/target 一致)而非给依赖 HAR 降级——引擎 HAR 无法由插件侧修改,且真机(Mate 60 Pro,ROM 7.0.0.111,API 26)恰好满足。

维护策略:插件 HAR 本身不锁定 SDK 级别(ohos/build-profile.json5 保持模板最小化),下限由 Flutter OH 引擎决定并随版本演进;README 的"约束与限制"章节必须随每次 SDK 对齐更新下限说明,避免用户按旧下限选型。

决策 4:查询型 API 用携带状态的 onSuccess

getRecentOperatingHandStatus 是"同步查询"语义(原生 motion.getRecentOperatingHandStatus() 同步返回),桥接到 Dart 后通过 invokeMethod<int> 返回状态码。选择扩展 GetRecentOperatingHandStatusOptions 并用必填 onSuccess(OperatingHandStatus) 承载结果,fail/complete 可选。理由:保持"零 Future"接口风格统一,同时与订阅类的无参 success 区分开。

维护策略:后续若有其他查询型 API(如能力检测),复用 onXxx 携带数据的命名约定。

决策 5:插件不主动请求运行时授权

DETECT_GESTURE / ACTIVITY_MOTION 由插件 HAR 声明,但插件不调用运行时授权弹窗——授权由宿主应用按目标系统权限规则完成(module.json5 声明 reason + usedScene,when: inuse)。理由:授权策略属于应用交互设计,插件层请求弹窗会与宿主的权限编排冲突。

维护策略:README 明确提示宿主需自行声明权限并给出示例配置链接;错误码 201(权限拒绝)在文档中对照说明。

决策 6:双平台文档与双 CHANGELOG

仓库同时维护 README.md(主文档,含开发环境/权限配置/运行示例/排查清单)与 README.OpenHarmony(_CN).md(OpenHarmony 适配文档,含下载安装/接口说明/遗留问题),以及 CHANGELOG.md 与 CHANGELOG.OpenHarmony.md。理由:OpenHarmony 三方库生态的惯例(见 oh-tpc 社区规范),适配文档独立成册便于社区巡检与用户按需阅读;发版说明双记录保证 pub.dev 与社区清单同步。

维护策略:每次发版同时更新四份文档;所有仓库地址统一 AtomGit,链接与 pubspec.yaml 元数据保持一致。


六、测试与验证

6.1 测试环境

项目版本
Flutter3.47.9-ohos-1.0.0(oh-3.47-dev)
Dart3.13.4
HarmonyOS SDK26.0.0(API 26)
IDEDevEco Studio 26.0.0
设备 ROMOpenHarmony-7.0.0.111(API 26)
真机HUAWEI Mate 60 Pro(ALN-AL00)

6.2 验证要点

  1. 静态检查 — flutter analyze 无错误(接口扩展、示例 UI、测试全部覆盖);
  2. 单元测试 — test/holding_method_channel_test.dart 覆盖通道名契约、getRecentOperatingHandStatus 状态码映射(1→left / 99→unknown)、错误码透传(PlatformException → fail + complete)、取消订阅成功回调;test/holding_test.dart 验证默认平台实例;
  3. 构建验证 — flutter build hap --debug 成功产出签名 HAP(auto-signing 自动写入签名配置);
  4. 真机安装 — hdc install 成功,aa start 启动,进程稳定运行;
  5. 真机事件流 — hilog 观察到真实状态事件:握持手状态变化: 3(双手)→ 2(右手)、操作手状态变化: 0(未识别)→ 1(左手)→ 0,原生 EventSink 直推 Dart 的链路完整工作;
  6. 插件注册 — GeneratedPluginRegistrant.ets 由构建自动生成并注册 HoldingPlugin。

6.3 真机验证命令速查

# 构建(自动签名)
cd example && flutter build hap --debug

# 安装
hdc -t <device-id> install build/ohos/hap/entry-default-signed.hap

# 启动
hdc -t <device-id> shell aa start -b com.nutpi.holding_example -a EntryAbility

# 观察插件日志
hdc -t <device-id> shell "hilog -x | grep -i HoldingPlugin"

# 截图(真机运行效果)
hdc -t <device-id> shell "snapshot_display -f /data/local/tmp/holding_example.jpeg"
hdc -t <device-id> file recv /data/local/tmp/holding_example.jpeg doc/assets/holding_example.jpeg

测试状态说明:本机 Flutter OH SDK(oh-3.47.9-ohos-1.0.0)的 flutter test 存在 “Can’t load Kernel binary: Invalid SDK hash” 的内核加载回归(多个工程同样挂起,且伴随其他工程遗留的 flutter_tester 孤儿进程干扰),Dart 层验证以 flutter analyze + 真机构建运行为准;该问题属于引擎/工具链层,建议向上游 atomgit.com/oh-flutter/flutter_flutter 反馈。


七、运行效果

真机运行截图(握持手/操作手双卡片 + 状态日志):

Mate 60 Pro 真机运行效果

真机 hilog 事件流(订阅后状态实时上报):

11-10 09:20:41.700 I HoldingPlugin: 握持手状态变化: 3
11-10 09:20:43.739 I HoldingPlugin: 握持手状态变化: 2
11-10 09:20:44.702 I HoldingPlugin: 操作手状态变化: 0
11-10 09:20:46.707 I HoldingPlugin: 操作手状态变化: 1
11-10 09:20:53.920 I HoldingPlugin: 操作手状态变化: 0

八、踩坑复盘

8.1 踩坑记录

踩坑点现象 / 报错根因与解法
flutter clean 工具链崩溃Oops; flutter has exited unexpectedly: "type 'Null' is not a subtype of type 'List<dynamic>' in type cast",堆栈指向 OhosModule.fromOhosProject(flutter_tools/src/ohos/application_package.dart:252),同时留下 flutter_01.logFlutter OH 工具链(oh-3.47-dev)在本插件工程上解析 ohos 构建数据的空指针 bug。解法:跳过 flutter clean,手动删除可再生缓存 rm -rf .dart_tool build 后重新 flutter pub get;该问题建议向上游 atomgit.com/oh-flutter/flutter_flutter 反馈
flutter test 内核加载失败Shell: [ERROR] Dart Error: Can't load Kernel binary: Invalid SDK hash.,随后 Could not prepare isolate,测试无限挂起引擎 flutter_tester 与测试内核 SDK hash 不匹配的工具链回归(多工程复现);且其他工程(speechrecognizer/gesturesshare)遗留的 flutter_tester 孤儿进程(最长挂 36 小时)会干扰诊断。解法:pgrep -fl "flutter_tester|dartvm" 找出孤儿进程并清理,改用 flutter analyze(静态验证)+ 真机构建运行(行为验证)替代单测运行
compatibleSdkVersion 过低hvigor 报错 The project's compatibleSdkVersion: 18 cannot be lower than the minimum compatible version required by the dependencies: @ohos/flutter_ohos,构建 6 秒即失败新版 Flutter OH SDK 产出的引擎 HAR 最低兼容版本为 26,宿主 5.1.0(18) 不满足。解法:example/ohos/build-profile.json5 的 compatibleSdkVersion 升为 26.0.0,同步更新所有 README 的"约束与限制"(最低兼容 API 18 → 26)
SDK 版本字段格式混淆在 compileSdkVersion/targetSdkVersion 填写旧格式 7.0.0(26) 会导致 hvigor 无法识别新版 DevEco/Hvigor 约定使用 26.0.0 数值格式;旧格式仅适用于 API 18 及以前的 x.y.z(n) 写法
权限 reason schema 校验失败module.json5 的 requestPermissions[].reason 写纯文本时报 schema 校验错reason 必须是 $string:xxx 资源引用(或含 {} 的占位符),usedScene 必填 abilities + when;字符串资源放在 entry 模块 resources/base/element/string.json
Pub 镜像环境变量前缀失效PUB_HOSTED_URL=https://pub.dev yes y | flutter pub publish 仍发布到 pub.flutter-io.cn 并报 package repository requested authenticationshell 管道中 VAR=x 前缀只作用于管道左侧第一条命令,flutter 在右侧拿不到变量。解法:先 export PUB_HOSTED_URL=https://pub.dev 再执行发布;发布必须指向 pub.dev,国内下载镜像(flutter-io.cn)不支持发布 API
交互式 y/N 确认未应答发布命令在非交互 shell 中报 Package upload canceled.flutter pub publish 的最终确认 y/N 在非交互环境直接取消。解法:yes y | flutter pub publish 显式应答;首次向 pub.dev 发布会等待浏览器 OAuth 授权,需在浏览器完成登录

8.2 高频坑速查(鸿蒙插件开发)

  • module.json5 权限 reason 字段 schema 校验失败 → 必须是 $string:xxx 资源引用,不能写纯文本;
  • ArkTS 编译报 Property 'xxx' does not exist → 对照已安装 SDK 的 .d.ts 确认字段名,不要凭记忆写 API;
  • 真机"不生效" → 先查 hilog 是否有插件日志(本文用 HoldingPlugin TAG),有日志无事件是设备能力问题(holdingHandChanged 依赖硬件与系统能力,API 满足不保证产生事件),无日志先查 GeneratedPluginRegistrant 是否注册插件;
  • 事件流型插件注意 onCancel 兜底注销系统监听,否则引擎解绑后系统回调仍指向已释放的 EventSink。

8.3 已知问题

  1. 仅支持 ohos 平台 — Android/iOS/Web 没有原生实现,其他平台调用抛 UnimplementedError(预期行为,文档已注明);
  2. 事件产生依赖设备能力 — holdingHandChanged 需要 DETECT_GESTURE 权限且设备硬件支持握持感知,订阅成功不代表一定有事件;
  3. 共享平台订阅 — 多个 Holding 实例共享同一平台订阅,不支持各页面独立持有原生监听,建议服务层集中订阅后分发。

8.4 未来优化

  • Android/iOS 平台实现 — 操作手状态可基于 Android 的手势检测近似模拟(握持手无等价能力,需声明能力差异);
  • 能力检测 API — 新增 isHoldingSupported(),订阅前检测设备是否具备系统能力(错误码 801 提前暴露);
  • 单位测试 CI — 引擎层 “Invalid SDK hash” 回归修复后,把 flutter test 纳入流水线门禁。

九、发布与总结

9.1 发布到 pub.dev

版本准备就绪后(pubspec.yaml 的 version 已升为 0.0.2、四份文档已更新、flutter analyze 通过),执行发布:

# 1. 发布前校验(dry-run,不实际上传)
flutter pub publish --dry-run

# 2. 正式发布:显式指向 pub.dev(国内下载镜像不支持发布 API)
export PUB_HOSTED_URL=https://pub.dev
yes y | flutter pub publish

发布要点:

  • dry-run 先行:校验包元数据、文件清单、license 与文档齐备性;本文 dry-run 剩余 2 个良性警告(未提交的 git 变更提醒、CHANGELOG.OpenHarmony.md 改名建议),不阻塞发布;
  • 发布目标必须是 pub.dev:环境变量 PUB_HOSTED_URL 若指向 pub.flutter-io.cn 等下载镜像,发布 API 会报鉴权失败;用 export 显式覆盖(注意 shell 管道中 VAR=x 前缀只作用于左侧命令,见第八章踩坑);
  • OAuth 授权:首次向 pub.dev 发布会等待浏览器登录授权,完成一次后凭证缓存,后续发布无需重复;
  • 发版纪律:“Publishing is forever; packages cannot be unpublished”——发布不可撤回,务必 dry-run 通过、真机验证完成后再执行;
  • 发版同步:version 升级需同时更新 CHANGELOG.md 与 CHANGELOG.OpenHarmony.md,pub.dev 包页面会渲染 README.md 与 CHANGELOG.md。

发布成功后,用户即可通过 Pub 依赖使用(含 Flutter 3.47.9/API 26 构建配置):

dependencies:
  holding: ^0.0.2

9.2 总结

从 0 到 1 实现一个鸿蒙感知类 Flutter 三方库,核心路径可以概括为 三步走:

1. 找对应 ── 对照鸿蒙 Kit 的 .d.ts 找到系统 API 与 Dart 接口的一一映射
            (本文:motion.on/off/getRecentOperatingHandStatus → 订阅/取消/查询)
2. 保契约 ── 通道名、方法名、错误码透传完全一致;先建事件流再发原生订阅
3. 补缺口 ── 引擎 HAR 决定 compatibleSdkVersion 下限时跟随上调;无等价能力的平台明确声明

对于 holding 三方库,本次工作涉及:

  • API 26 升级:compatibleSdkVersion / compileSdkVersion / targetSdkVersion 统一 26.0.0,验证环境 Flutter 3.47.9-ohos-1.0.0 + Dart 3.13.4 + DevEco Studio 26.0.0;
  • 能力扩展:对照 API 26 SDK 纳入 operatingHandChanged(订阅/取消)与 getRecentOperatingHandStatus(查询),新增 1 个 Dart 枚举、5 个平台接口方法、2 条通道、1 个查询选项类,测试/示例/四份文档同步更新;
  • 真机验证:Mate 60 Pro(API 26)构建、安装、运行,hilog 观察到真实握持手与操作手事件流;
  • 发布:dry-run 校验后发布 holding 0.0.2 到 pub.dev,仓库地址全部统一 AtomGit。

Dart 接口一次成型、扩展只做增量不破坏契约——这正是 Flutter 跨平台三方库生态的魅力所在。希望这篇从 0 到 1 的完整记录,能帮助你在鸿蒙上复刻任意一个感知类插件的开发。


参考文档

插件与仓库(AtomGit)

鸿蒙系统 API(MultimodalAwarenessKit)

Flutter 生态与发布

开源协议:本项目基于 Apache License 2.0 开源。


相关推荐

RN 鸿蒙安全三方库落地:react-native-digital-shield,密钥不出 TEE,AES-GCM/RSA-JWT 跨端身份打通

本文基于 RNOH 0.84 与 HarmonyOS API 26,自研数字盾库 react-native-digital-shield,把 ArkTS 专属的 HUKS、asset 能力经TurboModule 桥接给 JS:提供 AES-256-GCM 加解密与密钥托管、敏感资产存储、RSA-2048 签发校验 RS256 JWT,并以 JWK 公钥和 HS256 共享密钥实现跨端身份互认,密钥明文始终不出 TEE。文中还复盘 TurboModule 双侧注册、GCM 需拆分 16 字节

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 8

Flutter 修复鸿蒙 WebView Cookie 同名键丢失:离线回读与开发助手复核实录

修复了 Flutter 三方库 webview_cookie_manager 鸿蒙端因同名键误判导致的 Cookie 名称丢失问题,确保业务键如 path、domain 等在回读时正确保留。修复后,模拟器与真机测试均实现四组数据完整恢复,值中多等号也保持不变。补丁已提交至 CPF-Flutter 官方仓库,提升插件在鸿蒙平台的可靠性。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 10

Flutter 鸿蒙启动图撤销后编译失败:用 DevEco Code 复核三方库修复

本文针对 Flutter 鸿蒙启动图撤销时导致编译失败的问题,修复了 flutter_native_splash_ohos 库在移除启动图时误删仍被引用资源的缺陷。通过保留1×1透明图片并透传自定义模块路径,确保资源引用完整性,使工程可正常构建。修复已提交至官方仓库,支持幂等操作与多场景验证,提升开发体验。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 15

Flutter 接入鸿蒙 ArkWeb 预初始化:帮助页首开时机与主线程成本实测

为解决Flutter鸿蒙版webview_flutter无法在无视图状态下提前初始化引擎的问题,我在其原生实现中新增可选静态入口initializeWebEngine()。实测表明,在2秒预热窗口下,页面首屏渲染(FCP)耗时从中位数1791ms降至1019ms,显著提升首次打开体验;而在零延迟场景下,总路径耗时略有上升,需权衡预热时机与用户等待成本。该补丁已提交至CPF-Flutter官方仓库,支持社区协作优化。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 11

Flutter 优化鸿蒙 printing_ohos 多页 PDF 光栅化:原生资源释放与缓冲复用

针对鸿蒙端 printing_ohos 插件存在的页码映射错误与高内存占用问题,本文提出并实现三项优化:修正页码索引映射、引入原生资源分层释放机制、复用像素缓冲区。修复后,第三页显示正确,内存增长显著降低——在模拟器上PSS增量中位数下降65.8%,真机测试下降64.4%。完整补丁已提交至官方仓库,支持社区协作与问题反馈。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 8

React Native 鸿蒙权限审计实战:让每一项权限都有人认领

本文介绍一款纯 JS 编写的 React Native 鸿蒙权限审计工具,解决权限声明与使用对账难、最小权限集计算矛盾、审核必填字段缺失三大痛点。通过 module.json5 声明、使用台账与源码扫描三路输入对账,自动识别未使用权限、补全 reason/usedScene、生成可粘贴的最小清单,并支持 CI 卡控与风险分评估。工具内核独立,离线运行,兼顾准确性与安全性,助力应用高效通过审核。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 8

Cordova 鸿蒙 WebView 安全实战:能改的运行时拦,改不了的构建期审

本文通过 cordova-plugin-harmony-webguard 插件,解决 Cordova/HarmonyOS 应用中导航安全、引擎不可修改配置、JS 门面静默降级三大难题。基于运行时策略闸门(onPageStart + JS 预检)与构建期审计 CLI(扫描 MainPage.ets、config.xml 等),实现协议拦截、白名单校验、高危配置阻断,支持 CI 失败退出,将安全基线可视化、可测化,真正落地“能改的运行时拦,改不了的构建期审”。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 11

Flutter 鸿蒙供应链审计实战:让高危依赖卡在 CI 门口

本文介绍了一个基于纯 Dart CLI 与 Flutter Dashboard 的依赖审计工具,解决鸿蒙工程中三套锁文件(pubspec.lock、oh-package-lock.json5、package-lock.json)统一读取、离线快照与 OSV 在线情报选择、以及高危依赖通过退出码卡在 CI 的问题。工具以锁文件为事实依据,支持离线快照(4 条真实 GHSA)与在线查询,输出 SBOM(CycloneDX 1.5)、Markdown/JSON 报告,并通过退出码实现 CI 阻断,性能秒级

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 33

React Native 鸿蒙端侧 OCR 实战:TurboModule 与纯 TS 引擎的逐字契约

本文以鸿蒙真机上的 React Native 0.84 工程为例,详解三大难题:通过 TurboModule 接入 HarmonyOS Core Vision Kit 实现端侧离线 OCR;规避 Metro 预置头导致的 require 不存在问题;实现 npm 包与 HAR 双形态交付。项目采用 codegen 生成 ArkTS 接口,结合纯 TS 回退引擎,确保跨引擎逐字符一致,并通过 autolinking 支持无缝集成。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 30

Flutter 鸿蒙端侧 OCR 实战:让文字识别留在设备上

本文通过一个运行在鸿蒙真机上的 Flutter 插件工程,系统解决了三大核心问题:如何统一内存、文件、相册三类输入的识别链路;如何实现与 KMP 版本的 JSON 契约逐字节对齐;以及在系统不提供置信度时如何诚实处理。基于 HarmonyOS Core Vision Kit,插件实现端侧离线 OCR,全程无网络、无权限、无密钥,支持真实小票与截图识别,性能稳定(312~507ms),并通过 35 项单元测试与真机集成验证。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 49

跨端 RN 鸿蒙碰一碰分享库实战:基于 Share Kit 封装 react-native-ohos-knock-share,实现卡片、文件一碰传

本文介绍基于React Native的鸿蒙碰一碰分享库,解决RN开发者在鸿蒙生态中无法便捷使用Share Kit碰一碰功能的痛点。通过封装NFC/星闪触碰、3秒发送窗口、沙箱文件URI转换等核心逻辑,实现跨设备快速传递链接、文件、图片等内容。支持手机、平板、PC多端适配,提供回环演示与真机实测验证,显著降低接入门槛,提升跨端协作效率。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 37

React Native 鸿蒙实战:给应用接入小艺语音指令,跑通 Skill 声明到操作闭环

本文介绍如何在 React Native 鸿蒙(RNOH)工程中实现小艺技能闭环,通过开源库 react-native-ohos-ai-agent 封装技能声明、语音指令接收、端侧执行与结果回传全流程。针对三方应用权限限制,精准定位可操作边界,实现“被唤起→执行→反馈”链路贯通。结合五张真机截图验证:技能注册、指令匹配、意图上报、智能体调起及对话层弹出等关键节点均成功,完整打通语音交互闭环,助力应用无需入口即可被用户“叫”到。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 44

Flutter 鸿蒙实现精准碰一碰分享:tap_share_ohos 三方库打通真机传文件与传卡片

本文针对Flutter在鸿蒙系统上“碰一碰”分享功能缺失的痛点,基于DevEco Studio 26.0、HarmonyOS 7.0 API 26与Flutter 3.41.9实测环境,提出并实现开源库tap_share_ohos。该库封装NFC、Share Kit、权限管理及分布式能力,通过Dart门面层统一接口,利用MethodChannel与BasicMessageChannel对接原生ArkTS逻辑,实现文件与卡片的碰一碰分享。真机测试显示,可无缝调起系统分享面板,支持华为分享接力

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 41

Flutter 鸿蒙实战:用三方库接入小艺语音指令与智能体,跑通 Skill 声明到操作闭环

本文基于 flutter_ohos_ai_agent 三方库,实现 Flutter 应用一键调起鸿蒙小艺智能体。针对跨端框架对接鸿蒙 AI 能力的碎片化、环境差异等痛点,通过插件封装系统 Kit 能力,提供统一 Dart API。实测显示,模拟器与真机均支持文本/语音指令闭环,真机可完整调起小艺面板并回传事件。核心原理为:利用 ArkUI 承载页桥接 FunctionComponent,通过 MethodChannel 与 EventChannel 实现 Flutter 与原生能力双向通信,完成从语音识别到

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 41

RNOH 原生内存泄漏定位与治理:借助 LeakyGuard 实现图片解码降采样、LRU 字节缓存、资源生命周期管控,长列表图片页面峰值内存优化

本文面向 RNOH 0.84 长列表图片场景,剖析滑动数分钟后进程被系统杀掉的根因:原图解码尺寸失控、缓存按条数限流、PixelMap未显式 release。基于自研LeakyGuar,TurboModule 库,给出 desiredSize 真降采样、字节预算 LRU 缓存(逐出必 release)、资源登记与快照 diff、卸载即释放 Hook 的闭环方案,实测单图内存降 98.6%,24 图峰值由 1098.6MB 降至15.8MB。并总结双侧注册、ArkTS 语法差异、vp 转像

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 105

Flutter 鸿蒙空间音频实战:让音乐播放器的声音“长出方位感“

本文通过一个真实运行的 Flutter 工程,系统解析了在鸿蒙系统上实现空间音频的三大核心问题:基于双耳时间差(ITD)、声级差(ILD)和频谱整形的方位感算法;耳机切换与音频焦点管理;以及播放器与音效的集成。项目使用自研插件 spatial_audio,结合原生 HarmonyOS 接口,实现了跨设备的空间音频体验,支持普通耳机,无需硬件依赖,并提供实时参数反馈与系统状态监听。附带完整代码仓库与实测效果展示,为 Flutter on HarmonyOS 的音频开发提供了可复用的技术方案。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 272

Flutter 鸿蒙 HiTrace 请求归因:图片压缩等待时,主线程在做什么

为定位鸿蒙平台图片压缩性能瓶颈,我在 flutter_native_image 插件中新增可选的 HiTrace 阶段追踪功能,支持按请求开启解码、缩放、编码与资源释放阶段的耗时分析。通过对比模拟器与真机(华为畅享 100 Pro Max)数据,精准识别出耗时主要集中在解码与编码阶段,显著提升诊断效率。相关代码已提交至官方仓库,助力开发者快速定位性能问题。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Flutter 适配鸿蒙折叠屏铰链角度按需订阅:支架调节页用完即停与重入实测

本文介绍对 Flutter 三方库 dual_screen 的优化:针对支架调节页等短时观察场景,保留原有缓存入口的同时,新增按需订阅机制。通过按需启停监听,实现页面退出后停止原生回调,避免无效数据更新。实测显示,无消费者时原生回调从3条降至0条,重进页面则等待新事件,更符合临时工具页的使用逻辑。完整代码已开源,支持鸿蒙折叠屏角度动态感知,适用于需精准控制生命周期的折叠屏应用开发。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Flutter 适配鸿蒙手表屏幕形状:让计时卡片按圆屏和方屏布局

本文基于 wear_plus 1.2.4 版本,为鸿蒙系统补全屏幕形状查询功能,使 WatchShape 能正确返回 round 等形状信息。通过在原 Dart 接口不变的前提下,新增鸿蒙平台处理器,实现与 Android 一致的异步查询逻辑。对照实验验证:适配后页面可从“查询未接入”变为“圆屏已确认”,日志成功接收系统回执,布局行为保持一致。完整代码开源至 oh-flutter/wear_plus,支持 Flutter 鸿蒙穿戴设备开发。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Flutter 优化鸿蒙剪贴板大图复制卡顿:从主线程同步阻塞到异步文件描述符直解

针对鸿蒙系统下 flutter_image_clipboard 插件复制高清图片时阻塞主线程的问题,本文提出三项优化:异步文件描述符直解、异步剪贴板写入及原生资源逆序释放。实测显示,模拟器事件循环最大间隔从300.32ms降至54.36ms(降幅81.9%),真机上更从133.20ms降至11.20ms(降幅91.59%),显著提升页面响应性。完整代码已开源,支持直接使用与社区交流。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Flutter 三方库 petalmaps_ohos 的 OpenHarmony 适配实战(0 → 1 全记录)

本文详细记录了从零构建 Flutter 三方库 petalmaps_ohos 并适配 OpenHarmony/HarmonyOS 平台的全过程,涵盖项目初始化、原生实现(基于 ArkTS 和 MethodCallHandler)、插件注册、功能验证及升级至 HarmonyOS SDK 26 的关键步骤。重点解析了为何需实现 AbilityAware 获取 UIAbilityContext 以确保地图调起成功,并通过分层架构、枚举封装与兼容性设计,实现功能完整、易用且低维护成本的“调起系”地图插件,

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 52

Flutter 三方库 gesturesshare 从 0 到 1:鸿蒙隔空传送分享插件的开发、升级与发布实战

本文详述了 Flutter for OpenHarmony 三方库 gesturesshare 从零开发到发布全过程:基于 HarmonyOS 隔空传送能力,实现跨设备图片与 App Linking 一键分享。涵盖双端代码设计、API 26 升级、真机验证及 Pub 发布流程,提供 Dart/ArkTS 对照与踩坑复盘。项目填补 Flutter 生态空白,推动鸿蒙多设备交互能力普及,助力开发者快速接入系统级功能。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 26

Qt 鸿蒙沉浸光感实战:让玻璃的每个像素自己发光

本文通过一个 QML 模块与 Qt for OpenHarmony 构建实践,完整解答了三个核心问题:如何用 81 行 GLSL 实现无图片依赖的动态玻璃材质(含光核漂移、视差、SDF 圆角与颗粒);如何在 Qt Quick 中通过 C++ 控制器实现弹层锚定与翻转;以及如何将 Qt 应用交叉编译并以 HAR 形式部署至鸿蒙系统。项目基于 Qt 5.12.12,利用 ShaderEffect 实现纯渲染级视觉效果,兼顾性能与跨平台兼容性,提供可复用的开源方案。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 21

开源鸿蒙平台 KMP_CMP 三方库「端侧语音转文字」适配全流程

本项目实现KMP/CMP在OpenHarmony鸿蒙平台的语音转文字(TTS)完整链路适配,通过Kotlin/Native共享模型、C ABI/N-API桥接与ArkTS交互,解决权限、音频格式、生命周期及跨平台契约等难题。支持中文、英文、日文多语言识别,提供统一结果模型与JSON序列化,实测于华为Mate 80 Pro Max模拟器,验证了JVM测试、Native动态库、HAP签名部署及真机识别全流程。项目结构清晰,分六阶段推进,确保代码复用与平台无关性,为KMP在鸿蒙生态落地提供可复用的技术范式。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 30

开源鸿蒙平台 KMP_CMP 三方库「端侧OCR」新特性适配

本项目实现Kotlin Multiplatform(KMP)在OpenHarmony平台的端侧OCR适配,基于HarmonyOS 7.0 SDK与Compose Multiplatform 1.9.2,成功打通JVM、Kotlin/Native与ArkTS间的数据交互。通过C ABI/N-API桥接层,实现KMP模型与Core Vision Kit识别结果的跨语言共享,支持中、英、日、韩四语识别。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 11

开源鸿蒙平台 KMP/CMP 三方库「智感握姿」新特性

本项目实现鸿蒙KMP/CMP平台下智感握姿能力的跨端适配,基于Kotlin Multiplatform构建共享状态模型,通过C ABI与ArkTS桥接,支持左手、右手、双手等五种握持状态的稳定传递。项目打通ohosArm64目标、Native动态库构建与DevEco Studio集成链路,提供统一API与真机验证示例,确保状态模型、事件序列与生命周期在多端一致。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 16

Electron 鸿蒙沉浸光感实战:把玻璃交给操作系统去发光

本文通过一个 Electron 库与 HarmonyOS 三方库,系统解决三大核心问题:跨平台材质路径对齐(macOS vibrancy、Windows acrylic、鸿蒙 backgroundBlurStyle)、菜单子窗口精准定位(坐标转换与工作区适配)、以及无脚本菜单文档的安全事件回传(自定义 URL scheme 与焦点管理)。基于 CPF-Electron 鸿蒙化分支,实现桌面级玻璃效果——材质由操作系统合成器采样,网页仅渲染薄色层与光斑,确保视觉一致性与性能优化。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 21

Flutter 修复 flutter_secure_storage 鸿蒙入口路由与前缀隔离:让多账号登录凭证在手机上独立读写与清理

本文针对 flutter_secure_storage 在鸿蒙平台的适配问题,修复了主包缺失 TargetPlatform.ohos 路由导致的 unsupported_platform 错误,并统一鸿蒙子包多账号前缀的读写与清理范围,解决键值覆盖、前缀错用及批量操作匹配过宽等问题。修复后,业务层可继续使用统一接口实现跨账号安全存储,保障数据隔离性。完整代码已开源至 abaoge/flutter_secure_storage。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 9865

Cordova 鸿蒙沉浸光感实战:让弹层玻璃“喝到“背景的光

本文通过一个 HarmonyOS 模拟器上的 Cordova 项目,详解了在 WebView 中实现“玻璃拟态”效果的三大核心问题:利用 backdrop-filter 实现背景实时模糊与提亮,构建四层结构(采样、渐变底、光场、噪点)模拟真实玻璃质感;通过锚点定位与动态布局实现弹层贴边生长;并集成焦点管理、系统返回键及 inert 属性,确保交互礼仪完整。插件为纯 JS 无原生依赖,基于 ArkWeb 内核,兼顾性能与真实背景采样,适用于菜单、浮层等场景。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 25

CMP 鸿蒙透明玻璃实战:让悬浮导航栏真正透出光场

本文通过一个KMP/CMP工程,详解如何在鸿蒙上用Kotlin实现沉浸光感玻璃材质的逐像素渲染。核心解决三大问题:透明感源于环境光场与带alpha的柔光混合、玻璃“透光”依赖alpha保留的屏幕混合算法、跨平台一致性通过软件光栅器与原生桥接实现。项目以纯代码生成全屏光场与悬浮玻璃组件,无图片资源,支持动态切换调色板,实测模拟器下亚秒级重绘,完整复现官方“沉浸光感”视觉效果。仓库地址:https://atomgit.com/lqjmac/spatial-glass-cmp。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 20
上一篇: Flutter 三方库 gesturesshare 从 0 到 1:鸿蒙隔空传送分享插件的开发、升级与发布实战
下一篇: Flutter 三方库 petalmaps_ohos 的 OpenHarmony 适配实战(0 → 1 全记录)
AtomGit 代码君
AtomGit 代码君 企业官方账号 企业官方账号
博客等级 码龄6年 2万+粉丝 856原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值