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

📦 Flutter 三方库 dual_screen 是用于读取折叠屏布局与铰链角度数据的 package。在支架调节页中,原有鸿蒙实现会在页面退出后继续更新角度缓存。我在原库基础上保留原入口兼容,增加了随消费者连接和取消的按需订阅入口。
完整代码已开源在 oh-flutter/flutter-dualscreen,可直接使用。有问题可以到仓库提 Issue,也可以来 CPF-Flutter 鸿蒙社区 交流 Flutter 鸿蒙适配问题。

前言

我做了一个支架调节页,在 Mate X7 模拟器上切换折叠姿态,看着度数调整位置。点“返回首页”后,角度页已经销毁,首页也不用这些数据,可我继续折动屏幕时,原生日志仍在收到角度回调。

🔍 我顺着页面的取消调用往下查,发现 dual_screen 原入口会持续更新最近一次角度缓存,方便后续组件读取。这种用法适合常驻组件,但我的工具页只在打开时需要度数,于是我保留原入口,另外增加了按需订阅。用相同的动作再次采样,无消费者期间的原生回调从 3 条降到了 0 条,重新进页则先等待新的角度变化。

一、使用场景与痛点

1.1 支架调节页只需要短时观察角度

折叠角度订阅的四类常见使用场景

图1 使用场景

图 1 展示了四类常见的折叠姿态交互。半折悬停观影、桌面视频通话、帐篷模式时钟都可能读取铰链角度,但是否需要跨页面保留监听,要看具体页面的职责。我把本次示例限定在其中的调节环节:用户需要看度数时打开工具页,调好位置后关闭它,后续首页不依赖角度变化。表 1 按“离开后需求”和“再次进入预期”区分了这两类诉求。

常见情景示例场景离开后需求再次进入预期
辅助调整悬停姿态支架调节页停止角度观察随铰链微调获取新事件
硬件功能自检折叠能力检查页结束本次检查重新发起独立采样
开发联调核验临时传感器调试面板停止后台回调开启全新观察会话
全局布局或角标联动常驻状态面板继续维护最新值订阅后立即回放缓存角度

表1 订阅场景

表 1 前三类页面退出后可以结束观察,第四类则要继续维护随设备变化刷新的角度。这里有一个容易混淆的区分点——保留历史值不等于继续更新缓存:

  • 应用可以在停止监听的同时保存上一次读数,只是那份数字不再代表当前物理角度。
  • 原库选择持续监听,让页面离开期间的缓存也跟着硬件变化实时更新。
  • 我增加的按需入口则在最后一个消费者退出时清空会话缓存,下次进入等待新的传感器事件。

两种策略各自对应真实需求,接入前需要先确认页面属于哪一类。为了复现短时观察路径,我在 example/lib/lifecycle_probe.dart 中写了一个普通工具页,交互只有三步:

  1. 启动应用后停在“折叠工具”首页,界面只放说明文字和“打开折叠角度页”按钮,首页自身不创建角度订阅。
  2. 点击按钮压入“折叠角度”子页,先查询折叠屏能力,再订阅 hingeAngleEventsOnDemand,实时显示大字号度数与当前会话收到的事件条数。
  3. 点击“返回首页”弹出当前路由,触发 dispose() 取消订阅;再次进入角度页时作为新会话等待系统角度变化。

阅读示例时还要分清路由可见性与实例销毁的边界:

  • 路由出栈销毁:示例里的“离开”指返回首页并销毁角度页 State,从而在 dispose() 中触发取消。
  • 页面保活隐藏:如果用 Navigator.push 在角度页之上压入详情页,或者用 IndexedStack 保留后台标签,原页面的 State 仍在内存中,订阅不会自动释放,业务需结合路由监听或可见性回调主动调用 cancel()。
  • 职责范围:示例仅展示设备上报的角度读数,不控制物理支架,也不判断某个悬停角度是否安全。

1.2 查库核验

我检查了 oh-Flutter、CPF-Flutter 组织下的仓库,以及上游 microsoft/flutter-dualscreen 源码。这几个仓库各自负责不同部分:

  • 上游包维护方(microsoft/flutter-dualscreen):定义跨平台公开 API(TwoPane、MediaQueryData.hinge、DualScreenInfo)以及角度流的最近值缓存约定。
  • 鸿蒙社区适配维护方(oh-flutter/flutter-dualscreen):提供鸿蒙端插件工程,接通 display.isFoldable() 能力检测与 display.on('foldAngleChange') 事件通道(其中布局折痕数据仍受引擎侧约束,本篇仅核验角度通道),并接收本次按需订阅核心补丁与单测。
  • 公开复现分支(abaoge/flutter-dualscreen):在社区源码 PR 合入发版前,保留同款验证页与采样复现材料,方便读者直接拉取对照。

图 2 是 pub.dev 上的 dual_screen 1.0.4 包页面,可以看到发布方、Android 平台标记,以及双栏布局和铰链角度数据的用途说明。

pub.dev上的dual_screen 1.0.4包页面

图2 上游包页面

图 3 是鸿蒙社区适配仓的目录,包含 ohos、lib、example 和 test。选库时,我把上游包信息与这份鸿蒙实现对照核查,再确定需要补充的订阅策略。

鸿蒙社区flutter-dualscreen仓库目录

图3 社区适配仓

既然鸿蒙系统 API 与通道已经接通,就不需要另建重复包。至于原入口为什么退出页面后不断开原生监听,查阅上游变更记录可以看到 1.0.0+2 版本的明确说明:

DualScreenInfo.hingeAngleEvents now caches the last value.
—— 出处:microsoft/flutter-dualscreen 上游 CHANGELOG.md(1.0.0+2)

源码中的 _repeatLatest 在首次读取旧入口时订阅平台广播流,并缓存最新角度。页面取消时,只从消费者集合中移除当前页面,底层源订阅仍保留。因此我把按需启停做成可选能力,保留原来的缓存行为。

不过,支架调节页确实需要在退出时停止观察,而原入口走不通最后一步:

  1. 业务页面的取消请求停在 Dart 多播集合里,不会因最后一个页面退出而触发鸿蒙端的 display.off('foldAngleChange', this.foldAngleCallback)。
  2. 在引擎仍挂载、源流仍活跃的期间,后续折叠动作还会进入应用刷新那份无人读取的缓存(引擎卸载时的 onDetachedFromEngine 清理属于另一条生命周期路径)。
  3. 业务页面拿不到插件私有的源订阅对象,在包外再套一层自定义 Stream 也无法通知底层解绑。

因此,按需启停必须放回 dual_screen 内部实现:首个消费者出现时连接上游,同一会话内共享读数,最后一个消费者退出时清空缓存并取消上游订阅。

1.3 版本基线与改动范围

dual_screen按需订阅公开入口与模块配置对照

图4 入口与配置

我采用 oh-flutter/flutter-dualscreen 的 main 源码快照作为基线,包版本为 1.0.4,鸿蒙适配变更记录为 1.0.4-ohos-1.0.0-beta.1。准确提交映射保存在公开工程的来源清单中。

版本核对提示:该仓库在 pubspec.yaml 中沿用上游版本号 1.0.4,但 CHANGELOG.OpenHarmony.md 记录的是 1.0.4-ohos-1.0.0-beta.1(Beta 阶段适配)。pub.dev 上的 dual_screen 1.0.4 仅包含 Android 原生实现,复现时不能只看版本号相同就直接替换成中心仓依赖。

原仓库中文文档记录的早期验证环境为 3.44.9+ohos-0.0.1-canary1。迁到本文使用的 HarmonyOS 7(API 26)和社区稳定工具链时,我按下面的顺序处理:

  1. 增加按需订阅入口,保留旧入口的缓存与回放语义。
  2. 增加生命周期和路由测试,检查最后一个消费者退出及重新进入的行为。
  3. 调整宿主编译配置和设备声明,让同款页面能在当前模拟器运行。
  4. 为对照采样增加原生日志,只记录注册、解绑与角度回调,不改变监听逻辑。

补丁文件与应用顺序保存在仓库工程说明中,业务直接使用第五章的依赖即可。

导出的工程完整保留了原库 MIT 许可证、版权头、Android 实现、38 项原有单测以及 example/lib/main.dart 双栏示例。图 4 右侧展示了更新后的模块声明:

  • 插件 HAR deviceTypes 声明为 phone 和 tablet。
  • 示例宿主 compileSdkVersion 与 targetSdkVersion 设为 26.0.0,runtimeOS 为 HarmonyOS。
  • 宿主 requestPermissions 为空数组——监听折叠角度调用的是鸿蒙公开显示接口,不需要申请额外权限,顺手移除模板残留的 INTERNET 权限不影响调用路径。

二、按需订阅的实际效果

2.1 场景页进出前后的实测对照

图 5、图 6 采集自同一台 Mate X7(API 26)折叠屏模拟器。两组运行相同的 lifecycle_probe.dart 工具页,仅切换第 109 行订阅的公开 getter,分别对比原入口 hingeAngleEvents 与按需入口 hingeAngleEventsOnDemand。

原入口返回首页后后台仍缓存并在重入时立即回放

图5 原版页面动图

按需入口返回首页即停止观察并在重入后等待新变化

图6 按需页面动图

两组按完全一致的四步顺序操作:

  1. 从“折叠工具”首页点击“打开折叠角度页”,将模拟器切到半折叠(90.0°),再恢复展开(180.0°)。
  2. 点击返回回到首页,确认角度页路由销毁后,在首页无业务消费者期间再次将模拟器切为半折叠(90.0°)。
  3. 第二次点击“打开折叠角度页”,手不触碰铰链,直接观察刚进入页面时的初始读数与事件计数。
  4. 在第二次会话中将模拟器展开到 180.0°,观察页面实时刷新情况后返回首页。

原入口第二次进入页面直接回放离开期间缓存的90度

图7 原版重入回放

按需入口第二次进入页面初始处于等待设备角度变化状态

图8 按需重入等待

按需入口在新会话折动屏幕后实时刷新为180度

图9 按需实时刷新

第二次进入角度页时,两组表现就不同了。画面中央的浅蓝竖条是 DevEco 模拟器的折痕避让区提示。

  • 原入口(图 5、图 7):刚打开页面就立即显示 90.0°,“本次页面收到 1 条角度数据”。此时尚未做新的折叠动作,这条读数来自停留首页期间后台继续更新的静态缓存。
  • 按需入口(图 6、图 8、图 9):刚打开页面时显示占位符 — 与“等待设备角度变化,本次页面收到 0 条角度数据”。上一轮退出后插件已停止接收首页期间的角度变化,也没有保留旧值;直到随后展开屏幕,页面才实时刷新为 180.0°(图 9)。

表 2 列出了这些截图与日志对应的软硬件环境。DevEco 设备管理器显示的镜像号为 7.0.0.107,而通过系统属性 const.ohos.fullname 回读的字符串为 OpenHarmony-7.0.0.105,表中分开记录,避免混淆两个不同字段。

环境项实测配置
Flutter / Dart3.41.9+ohos-1.0.2 / 3.11.5
DevEco Studio26.0.0.865
HarmonyOS SDK26.0.0.105 Release(API 26)
模拟器模板Mate X7,arm64,4 核、4 GiB
模拟器镜像版本7.0.0.107
设备系统回读版本OpenHarmony-7.0.0.105
构建与安装方式Debug 未签名 HAP(仅用于本机模拟器调试)
观测与采样工具模拟器官方折叠控制命令、hdc、HiLog、自动采样器

表2 验证环境

2.2 固定时序下的原生注册与回调计数

只看界面读数还不能确认底层 display 监听是否真正解绑。我另外使用 lifecycle_runner.dart 在同一台 Mate X7 上跑固定时间表采样,消除人工点击快慢带来的时序偏差。这里记录的是原入口 baseline-02 与按需入口 ondemand-01 各一次约 40 秒的运行记录(单次运行内部包含两轮订阅,并非多轮实验取均值):

  1. 启动准备(0~3 秒):启动采样器并完成设备折叠能力检查。
  2. 第一轮活跃期 active_1(3~13 秒):建立订阅保持 10 秒,在第 6、10 秒切换折叠姿态。
  3. 第一段空闲期 idle_1(13~23 秒):取消订阅静置 10 秒,在第 17、21 秒无消费者状态下切换折叠姿态。
  4. 第二轮活跃期 active_2 与收尾空闲 complete_idle(23~40 秒):第 23 秒重新订阅保持 10 秒(第 27、31 秒切换姿态),第 33 秒再次取消并在第 37 秒做最后一次空闲姿态切换。

七次 90°/180° 折叠命令在两组中按相同计划时点触发,统计结果见表 3。

观察指标原入口 hingeAngleEvents按需入口 hingeAngleEventsOnDemand
业务层订阅/取消次数2/22/2
鸿蒙原生注册/注销成功次数1/02/2
无业务消费者期间原生角度回调30 🔴↓
采样结束时注册减注销差额10 🔴↓
原生层收到的系统角度事件总数74
业务层收到的角度回调总数54

表3 实测计数

拆开看表 3 的三组核心差异:

  • 空闲期原生回调(3 对 0):第 17、21 秒落在 idle_1,第 37 秒落在 complete_idle。原入口一直保留系统监听,七次切换全部记入 nativeAngle;按需入口在退出活跃期时调用了 display.off,只接收两段活跃期内的四次变化,空闲期间为零。
  • 业务层事件数(5 对 4):按需入口两轮各收一次 90° 和一次 180°,合计四条;原入口多出的第 5 条,是第 23 秒第二次订阅建立后回放的 180.0° 缓存。
  • 注册减注销差额(1 对 0):差额按 nativeRegistered 减去 nativeUnregistered 计算。原入口最终为 1,说明它只维持唯一一条共享监听,不会随页面进出无限累加;按需入口两进两出完成 2/2 配对,结束归零,代价是比原入口多执行一次注册和两次注销。

我没有测量功耗、堆内存、CPU 或电流。应用调用 display.off 只代表当前插件解除了自己的回调,系统桌面等服务仍可能读取铰链状态,不能推断物理传感器已经断电,因此上述计数仅用于核验订阅生命周期,不换算成节电比例或性能提升幅度。

2.3 从真实 HiLog 核对原生解绑时序

两组采样包都加入同一份观测日志,记录请求监听、注册成功、请求取消、解绑成功和收到角度五类事件。图 10 左侧为 baseline-02 的 26 行原始记录(PID 21851),右侧为 ondemand-01 的 28 行原始记录(PID 23207)。

两轮订阅生命周期真实HiLog对照

图10 原生日志对照

原入口日志时序:取消停留在多播层

对照图 10 左栏行号,原入口在两轮订阅中的关键轨迹如下:

  • 首次注册:第 06、07 行(03:31:49.977~978)打印唯一一次 nativeListen 与 nativeRegistered。
  • 首次取消与空闲穿透:第 12、13 行(03:31:59.982~984)业务层打出 cancelRequested、cancelReturned 后,原生层没有出现 nativeCancel;随后空闲期的 03:32:04.838(第 14 行)和 03:32:08.954(第 15 行)照常收到 angle=90 与 angle=180。
  • 重入回放与尾声回调:到 03:32:09.990(第 17、18 行),第二轮订阅返回与 angleReceived angle=180.0 total=3 落在同一毫秒。缓存实际通过异步事件投递,毫秒级时间戳不足以区分先后,不能把它当作同步回调。到第 25 行 runnerComplete 结束四秒多后的 03:32:24.786(第 26 行),原生层仍在接收第七条 nativeAngle angle=90。
按需入口日志时序:取消透传至原生解绑

再看图 10 右栏行号,按需入口将业务取消一路传到了鸿蒙显示模块:

  • 首次取消即解绑:第 12、13 行发起第一次取消,Dart 在 03:33:22.861 打印 cancelReturned;下一毫秒(03:33:22.862,第 14、15 行),原生层紧接着打出 nativeCancel 与 nativeUnregistered,此后十秒空闲窗口内没有任何 nativeAngle。
  • 二次会话成对收尾:第二轮订阅于 03:33:32.864(第 18、19 行)重新注册,接住新的 90°、180° 变化后,在 03:33:42.867~870(第 27、28 行)完成第二次解绑。

日志核验提示:业务侧的取消 Future 不等待原生解绑完成。EventChannel 内部虽然等待取消回包,但广播控制器的取消回调不返回 Future,这段等待没有传回业务侧。应结合原生解绑成功日志和空闲期零回调核验;这里只比较同一份 HiLog 的先后顺序,不跨时钟相减计算通道耗时。

三、原理与核心代码

3.1 先把四层订阅链路拆开

顺着页面手里的订阅对象往下追踪,一次角度监听横跨了从业务 Widget 到鸿蒙系统模块的四个层级:

  1. 第 1 层(业务消费层):具体页面持有的 StreamSubscription<double>,随页面生命周期建立与取消。
  2. 第 2 层(Dart 多播与缓存控制层):DualScreenInfo 用 Stream.multi 封装的分发器,决定何时连接上游以及是否保留最近一次角度。
  3. 第 3 层(共享平台广播源):EventChannel.receiveBroadcastStream() 建立的跨端广播流,按上游订阅计数向原生层发送 listen 或 cancel。
  4. 第 4 层(鸿蒙原生显示监听):ArkTS 插件 DualScreenInfo.ets 在 @ohos.display 上注册与解绑 foldAngleChange 回调。

图 11 标出了原入口与按需入口在第 2 层的分流位置。

四层订阅链路与鸿蒙折叠角度监听生命周期对照

图11 四层订阅链路

页面在 dispose() 中取消的只是第 1 层到第 2 层的连接:

  • 原入口 _repeatLatest 收到取消后,仅把当前页面从 currentListeners 移除,始终保活第 3 层平台源。
  • 新入口 _repeatLatestWhileListening 在首个消费者加入时才向第 3 层发起 listen,最后一个消费者退出时主动取消第 3 层订阅。只要进程内从未激活过旧入口,第 3 层监听数归零便会通过 EventChannel 发送 cancel,驱动第 4 层执行 display.off('foldAngleChange', this.foldAngleCallback);反之,若旧入口已被其他组件读取,其常驻订阅仍在,按需入口不会越权替它断开共享源。

3.2 鸿蒙原生层已有的注册与回调引用解绑

社区原有的 DualScreenInfo.ets 已负责原生启停:引擎挂载时建立角度通道,收到监听请求时注册,收到取消请求或引擎卸载时解绑。下面摘录完整角度回调与收尾函数,仅补充中文注释;其余通道配置见完整原生源码。

  private readonly foldAngleCallback = (angles: Array<number>): void => {
    if (angles.length === 0) {
      return;
    }

    // Dart 接口只传一个角度,多折轴设备按系统约定取第一轴。
    this.hingeAngleSink?.success(angles[0]);
  };

注册函数先清理旧监听,再检查设备是否可折叠。非折叠设备直接结束流;可折叠设备保存事件接收端,注册角度回调。注册抛错时记录系统错误并通知 Dart 订阅者。收尾函数如下:

  private stopListening(): void {
    if (this.isListening) {
      try {
        // 传回同一回调引用,只解除本插件的监听。
        display.off('foldAngleChange', this.foldAngleCallback);
      } catch (error) {
        const businessError = error as BusinessError;
        hilog.warn(
          0,
          TAG,
          `Failed to stop fold angle listener: ${businessError.code} ${businessError.message}`,
        );
      }
    }

    this.isListening = false;
    this.hingeAngleSink = null;
  }
回调引用与解绑边界

审查原生代码时,首先要确认 display.on 与 display.off 传入的是不是同一个函数对象引用:

  • 插件将箭头函数保存在只读属性 foldAngleCallback 中,解绑时显式传回同一引用,只精准移除本插件注册的监听。
  • 如果省略 display.off('foldAngleChange') 的第二个参数,系统会清空该事件名下全部已注册回调,可能误伤宿主或其他 SDK;反之若每次临时传入新的匿名函数,又无法匹配已有回调导致解绑失效。
  • 注册前先清理旧监听、检查设备能力,避免重复注册。这部分沿用社区实现。
异常分支与单轴协议约束

注册和解绑异常走的是两条不对等的通知路径,排查时需要留意:

  • 注册失败:记录系统错误,并推入 Dart 流的错误回调,页面可以显示失败提示。
  • 解绑失败:只记录原生告警,然后清空本地监听状态和事件接收端。异常不会回传给 Dart,排查时需要查看 HiLog。
  • 单轴协议:系统返回角度数组,公开接口却只传一个浮点数,因此插件取数组第一项。按系统文档,多折轴设备在充电口朝下时以最右侧铰链作为第一轴;一个角度不能代表三折屏的完整姿态。

3.3 两个公开入口为什么要共用一个平台广播源

新增 hingeAngleEventsOnDemand 后,同一进程内可能既有旧入口消费者、又有新入口消费者。我没有让两个 getter 各自调用 _hingeAngleEventChannel.receiveBroadcastStream(),原因在于 Flutter BinaryMessenger 在同一通道名称上只维护一个消息处理器(setMessageHandler):如果两份广播流各自创建底层通道订阅,后激活的一方会覆盖前一方的处理器,任意一方取消时还可能把另一方正在使用的处理器直接清掉并关停原生监听。

因此,两个入口共用一个平台广播源,只在上层采用不同的缓存策略。下面保留共享源及新 getter;旧入口仍把同一源交给原来的缓存函数,完整声明见插件源码。

// 两个入口共用同一平台流,避免覆盖同名通道的处理器。
static final Stream<double> _platformHingeAngleEvents =
    _hingeAngleEventChannel.receiveBroadcastStream().map(
      (event) => (event as num).toDouble(),
    );
static Stream<double>? _onDemandHingeAngleEvents;

static Stream<double> get hingeAngleEventsOnDemand {
  try {
    return _onDemandHingeAngleEvents ??=
        _repeatLatestWhileListening(_platformHingeAngleEvents);
  } catch (e) {
    // 沿用入口的同步异常兜底;运行中的流错误仍交给订阅者处理。
    return Stream.empty();
  }
}

这里还有两处设计细节值得说明:

  • 惰性映射:_platformHingeAngleEvents 上的 .map(...) 只做数值转型,保留广播流的惰性特征;只要下游没有人调用 listen,就不会激活 EventChannel。新入口直接命名为 hingeAngleEventsOnDemand,阅读业务代码时一眼就能分辨当前页面采用了哪种策略,无需引入隐式全局开关。
  • 错误传递范围:getter 外层的 try ... catch 仅兜底创建流对象时的同步异常;原生侧通过 events.error 发出的错误走异步流错误通道,业务页面仍须在 listen 中传入 onError,而通道激活或停用阶段的平台异常则交由 Flutter 框架错误报告机制处理。

3.4 最后一个消费者退出时怎样安全收尾

原入口的行为已在第一章说明。这里看新增缓存函数中的两个完整处理单元:连接上游的局部函数,以及最后一位消费者取消时的回调。完整外层函数仍在插件源码中,下面按职责分开阅读。

外层保存消费者集合、最近值、当前订阅、取消中的 Future 和会话编号。新消费者加入集合后会收到本会话缓存,再尝试连接上游;如果源流已经结束,则直接关闭该消费者。

    void startListening() {
      // 没人需要、已经连接或仍在收尾时,都不重复连接。
      if (done ||
          currentListeners.isEmpty ||
          subscription != null ||
          cancelling != null) return;
      final session = ++generation;
      subscription = original.listen(
        (event) {
          if (session != generation) return;
          latest = event;
          for (var listener in [...currentListeners]) listener.addSync(event);
        },
        onError: (Object error, StackTrace stack) {
          if (session != generation) return;
          for (var listener in [...currentListeners])
            listener.addErrorSync(error, stack);
        },
        onDone: () {
          if (session != generation) return;
          done = true;
          latest = null;
          subscription = null;
          final listeners = [...currentListeners];
          currentListeners.clear();
          for (var listener in listeners) listener.closeSync();
        },
      );
    }

连接前的检查避免重复订阅,也会等待上一轮 Dart 取消流程结束。每个回调都检查会话编号,旧会话的迟到数据和错误因此不会更新当前消费者;结束回调则关闭现有消费者并记住终态。

      controller.onCancel = () async {
        currentListeners.remove(controller);
        if (done || currentListeners.isNotEmpty) return;
        // 最后一个业务订阅退出后,旧角度不再作为下一次采样的实时值。
        latest = null;
        final previous = subscription;
        if (previous == null) {
          await cancelling;
          return;
        }
        subscription = null;
        generation++;
        // 等 Dart 上游订阅取消后再重订;该 Future 不等待原生 cancel 回包。
        cancelling = previous.cancel().whenComplete(() {
          cancelling = null;
          startListening();
        });
        await cancelling;
      };

取消回调先移除当前消费者。仍有人使用时继续监听;最后一人退出才清空缓存、使旧会话失效并取消上游。等待期间若有新消费者进入,收尾后会重新检查并连接,不会漏掉这次订阅请求。

会话怎样切换

为了看清 _repeatLatestWhileListening 在快速进出页面、并发监听和流终态之间的切换关系,图 12 用状态流转图梳理了它的完整会话状态机:

按需流会话状态流转图

图12 会话状态机

图 12 还标出了快速重进、回调内取消和流结束时的边界。

快速退出重进时的取消衔接

上一轮 Dart 取消流程未结束时,新消费者先留在集合中等待。收尾后,如果消费者仍在,就建立新连接;如果它也已退出,则保持空闲。这里等待的是 Dart 侧取消 Future,不能据此判断鸿蒙原生解绑已经完成,实际时序见第二章日志。

会话代次与结束路径

会话编号只能隔离 Dart 已识别的旧订阅回调。跨端传来的数据只有角度,没有原生会话号;若旧硬件事件到下一次连接后才送达,Dart 无法仅靠编号判断它属于哪一轮。因此,真正停止系统观察仍依赖原生解绑。

回调中取消订阅

向消费者派发事件、错误或关闭信号时,代码统一遍历 [...currentListeners] 浅拷贝快照。如果某个消费者采用 .first 或在收到首条角度时立刻在回调里执行 cancel(),直接遍历原集合会触发并发修改异常,快照遍历则能安全隔离。

非折叠设备的流终态

非折叠设备会由原生端返回流结束信号。状态机随后进入 Done 终态,关闭现有消费者;后来加入的订阅也直接结束,不会反复连接一条已终结的平台流。

3.5 四十九项库测试与路由测试怎样把关

工程保留原库 38 项单测,新增 11 项库测试(合计 49 项),外加独立 Example 的 2 项 Widget 路由测试。由于 DualScreenInfo 持有静态缓存流——旧入口一旦被读取就会在当前 isolate 留下持久订阅,平台流一旦触发 onDone 也无法重新打开——如果把所有用例塞进同一个测试文件,执行先后顺序就会互相污染。

我利用 flutter test 按测试文件隔离 Dart isolate 的机制拆分四组测试入口:

  • test/dual_screen_lifecycle_test.dart(8 项常规生命周期用例):覆盖惰性启动、末位取消、同会话共享回放、跨会话清缓存、快速重订、等待期中途退出及错误多播。内部通过 Completer 控制取消回包时机,验证平台通道严格按 ['listen', 'cancel', 'listen'] 顺序收发请求,且不把原生回包完成当作 Dart 取消 Future 的前置条件。
  • test/dual_screen_completion_test.dart(1 项独立终态用例):单独在独立 isolate 中验证 endOfStream 关闭当前消费者并拒绝后续重启,避免 done = true 终态污染其他生命周期用例。
  • test/dual_screen_legacy_compatibility_test.dart 与 test/dual_screen_mixed_lifecycle_test.dart(各 1 项混用兼容用例):分别验证“先读取旧入口再使用按需入口”与“按需采样中途加入旧入口”时的共享保活与取消隔离行为。
  • example/test/lifecycle_probe_test.dart(2 项页面路由用例):挂载 HingeLifecycleProbeApp,检查首页零订阅、进入角度页刷新读数、返回首页触发取消、重入显示等待态,以及能力查询抛错时不发起监听。

这些主机测试通过平台通道替身检查 Dart 状态机和路由交互。鸿蒙端的实际解绑时序,我另外用第 2 节的模拟器日志核对。

四、踩坑与边界条件

4.1 混用旧入口、暂停订阅与构建路径排查

接入后如果发现页面已经调用 cancel()、但日志行为与预期不一致,可以优先对照表 4 排查调用点与工程配置。

容易踩坑的现象背后的真实原因建议的处理方式
当前页面已改用 hingeAngleEventsOnDemand 且已退出,但 HiLog 里看不到 nativeUnregistered进程内其他组件(如常驻角标)曾读取过旧入口 hingeAngleEvents,其持久订阅保活了共享广播源全局检索 hingeAngleEvents 调用点;若全应用只需按需观察,统一迁移至新入口
页面隐藏后只暂停订阅暂停只暂缓 Dart 事件派发,底层仍监听不再需要数据时取消,下次显示再订阅
在含中文目录名的路径下执行 flutter build hap 报 00306003 Invalid project path当前版本 DevEco / Hvigor 构建工具链对工程路径字符集有限制构建前用 rsync 或克隆到纯英文 ASCII 路径再编译
原库模板用 API 26 SDK 编译安装时报设备类型或残留权限提示原示例 module.json5 设备类型为 default 且带了未使用的 INTERNET 权限按目标设备声明 phone/tablet,并删除未使用的网络权限

表4 接入排查

图 13 把前三种订阅操作放在一起比较。是否能释放原生监听,既取决于当前页面是否取消,也取决于共享源上还有没有其他订阅。

暂停、按需取消与新旧入口混用时的原生监听状态对照

图13 取消边界

⚠️ 我的接入提醒:最隐蔽的情况是应用其他角落曾读取过一次旧入口。例如首页顶栏一直靠 hingeAngleEvents 维护分屏角标,此时支架调节页退出就不应强行切断共享通道,否则顶栏也会失灵。正因如此,我没有设计 forceStopAllHingeListeners() 这类强制全局断流接口;业务应先查清哪些组件仍在消费角度,再决定统一迁移还是新旧并存。

4.2 重新进入角度页可能先看到等待占位符

图 8 展示的初始等待态是按需策略的直接表现:上一轮退出清空了 latest,再次注册 display.on('foldAngleChange') 后尚未收到新的角度变化事件,界面便显示 — 与“等待设备角度变化”(该监听接口本身不同步返回当前度数,物理真机注册瞬间是否会主动推送一次初值,我还没有在真机上验证)。围绕初始显示与能力查询,页面处理遵循三条原则:

  • 不用假数字填充未知角度:示例没有把初始空值写成 0.0° 或 180.0°,避免把设备尚未上报的度数展示成真实读数。
  • 不用折叠状态反推角度:display.getFoldStatus() 只返回展开、折叠或半折叠等离散状态,同一个半折叠状态可能对应 75° 或 115°,不能据此计算角度。
  • 区分不支持与查询异常:鸿蒙端以 display.isFoldable() 响应 hasHingeAngleSensor,而原库 Dart 实现会把通道异常捕获并转换成 false。调用方仅凭 false 无法区分“非折叠设备”和“通道查询失败”,因此示例文案写为“未能确认设备具备折叠角度能力”,不断言硬件缺失传感器。

五、结论与可直接复用的建议

5.1 适合哪些页面

这次扩展让短时查看角度的页面可以“用完即停”。在 Mate X7 模拟器的同条件采样中,无消费者空闲期的原生回调由 3 条降为 0 条;重新进入页面后,等待新的角度事件再更新读数。常驻组件仍可沿用旧入口的缓存与回放行为。

选择按需入口前,先确认页面能够接受短暂等待新读数。若界面需要保留上次结果,可以自行保存并标明“上次记录”;不要把历史值显示成刚从设备获取的角度。

5.2 添加依赖并订阅

核心改动已提交到社区仓库 PR #1。在合入发版前,应用可以直接依赖下面的公开固定提交,包名和导入路径仍是 dual_screen:

dependencies:
  dual_screen:
    git:
      url: https://atomgit.com/abaoge/flutter-dualscreen.git
      ref: 0ae1d75c79945e390f6dd9296770779745ac57d3

导入 package:dual_screen/dual_screen.dart 后,在页面初始化时调用下面的启动函数。示例沿用 State 的三个成员:_subscription 为可空 StreamSubscription,_angle 为可空 double,_status 初始为“正在查询设备能力”。订阅类型从 dart:async 导入,页面使用 Flutter Material 库。

角度页查询能力、订阅角度并在路由销毁后释放原生监听的时序

图14 页面接入

下面保留启动与销毁两个完整方法,去掉了采样计数和访问编号;查询失败、流错误及取消异常仍各自处理。

  Future<void> _start() async {
    // 原库会把能力查询异常转换为 false,不能据此区分不支持和查询失败。
    final supported = await DualScreenInfo.hasHingeAngleSensor;
    if (!mounted) return;
    if (!supported) {
      setState(() => _status = '未能确认设备具备折叠角度能力');
      return;
    }
    setState(() => _status = '等待设备角度变化');
    _subscription = DualScreenInfo.hingeAngleEventsOnDemand.listen(
      (angle) {
        if (!mounted) return;
        setState(() {
          _angle = angle;
          _status = '已收到设备角度';
        });
      },
      onError: (Object error) {
        if (mounted) setState(() => _status = '角度读取失败:$error');
      },
      onDone: () {
        if (mounted) setState(() => _status = '设备角度数据流已结束');
      },
    );
  }

  
  void dispose() {
    // dispose 无法等待 Future,但仍观察取消失败,避免吞掉清理异常。
    _subscription?.cancel().then<void>(
      (_) {},
      onError: (Object error, StackTrace stack) {
        debugPrint('取消角度订阅失败:$error\n$stack');
      },
    );
    super.dispose();
  }

能力查询返回时页面可能已经关闭,因此查询后先判断页面是否仍挂载。取消操作发生在同步销毁方法里,无法等待完成,但仍记录取消异常和堆栈。页面数据流结束时更新提示,避免界面一直显示“等待”。

这里的“离开”指路由出栈并销毁页面。如果页面只是被新路由盖住,或保留在标签页中,就要在可见性变化时主动取消,重新显示时再订阅;不能只依赖销毁回调。

5.3 接入时注意什么

  1. 检查是否混用了旧入口。 进程内只要还有旧入口的持久订阅,共享原生监听就会继续工作。不要为了关闭当前页面,强行断开其他组件的数据源。
  2. 区分暂停和取消。 暂停只影响 Dart 事件派发。页面不再需要角度时应取消,恢复使用时重新订阅。
  3. 按实际返回显示状态。 能力查询返回 false 可能是不支持,也可能是查询异常;新会话未收到角度时保持等待提示。多折轴设备、真机初始推送和后台挂起尚未验证。

完整 Example保留了页面与日志处理;构建、测试和对照采样放在仓库复现说明。业务只需添加依赖、使用新入口并管理自己的订阅,无需重新应用补丁。

小结

支架调节页需要的是打开时有读数、关闭后结束观察。把这项选择放进库的显式入口后,应用能按页面用途选择订阅方式,也能保留原有常驻组件的行为。

参考资料

  1. dual_screen 鸿蒙社区适配仓(oh-flutter/flutter-dualscreen) 与 按需订阅核心补丁 PR #1
  2. 同款验证页与采样复现分支(abaoge/flutter-dualscreen)
  3. dual_screen 上游变更记录(microsoft/flutter-dualscreen CHANGELOG)
  4. Flutter EventChannel.receiveBroadcastStream 官方文档
  5. HarmonyOS ArkTS API:@ohos.display(屏幕属性与折叠角度监听)

相关推荐

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大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 16

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大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 126

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

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

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

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 适配鸿蒙手表屏幕形状:让计时卡片按圆屏和方屏布局

本文基于 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 三方库 holding 的 OpenHarmony 适配实战:从 0 到 1 开发一个鸿蒙感知插件

本文详述了从零实现开源 Flutter 插件 holding 并适配 OpenHarmony/HarmonyOS 的全过程。基于鸿蒙专属 MultimodalAwarenessKit,实现握持手与操作手状态感知,覆盖事件流订阅、状态查询及 API 26 新增接口,确保功能一致、性能高效、接口稳定。通过 Dart 层封装、ArkTS 原生桥接、真机验证与单元测试,完成插件工程化落地,并发布至 pub.dev,为同类系统能力插件的鸿蒙化提供可复用的技术路径。

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

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大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

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 适配鸿蒙手表屏幕形状:让计时卡片按圆屏和方屏布局
下一篇: Flutter 鸿蒙 HiTrace 请求归因:图片压缩等待时,主线程在做什么
AtomGit 代码君
AtomGit 代码君 企业官方账号 企业官方账号
博客等级 码龄6年 2万+粉丝 856原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值