Flutter 三方库「flutter-dualscreen」的鸿蒙化适配指南

开发工具: 华为云码道

本文配套仓库(预定地址): oh-flutter/flutter-dualscreen

dual_screen 提供双区域布局与折叠屏铰链角度接口,应用可以调整主副区域的排列,并监听设备折叠角度。本文以 dual_screen 1.0.4 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

原生折叠能力与角度接口声明 Android、OHOS;TwoPane 的普通布局可用于其他 Flutter 平台,不能把普通布局能力等同于原生角度支持。配套仓库地址统一规划为 oh-flutter/flutter-dualscreen;尚未创建或同步时,先使用本地源码。本文依据本地 1.0.4 适配工作区,当前 HEAD 为 8836cb5ed8c734e2a51c5d4159573611d2117dd2,OHOS 相关实现还包含未提交内容,该 HEAD 不能单独还原本文代码。


真机运行图(从左到右):左右布局/上下布局/单区域。采集于 2026-09-09,设备 ALN-AL00 / HUAWEI Mate 60 Pro,系统 OpenHarmony 6.1.1.120(API 24);使用本地当前源码构建的签名 Release HAP。

本机是非折叠手机,三图展示布局切换,不作为铰链角度的验证证据。
截图标注:操作步骤与截图命令

执行目录:本文 Markdown 所在目录。先用 hdc list targets 获取设备 ID,将下列 <device-id> 替换为实际值。截图使用仓库完整 Demo;snapshot_display 在本机要求 .jpeg 后缀,图片保持原始真机画面。

bash 复制代码
mkdir -p blog-assets/dual_screen

左右布局: 选择"双区域"和"左右",保留主、副区域并排布局。

bash 复制代码
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/dual_screen-horizontal.jpeg
hdc -t <device-id> file recv /data/local/tmp/dual_screen-horizontal.jpeg ./blog-assets/dual_screen/horizontal.jpeg

上下布局: 保持"双区域",切换"上下",比例保持 50%。

bash 复制代码
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/dual_screen-vertical.jpeg
hdc -t <device-id> file recv /data/local/tmp/dual_screen-vertical.jpeg ./blog-assets/dual_screen/vertical.jpeg

单区域: 选择"主区域",查看单一内容区。

bash 复制代码
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/dual_screen-single-pane.jpeg
hdc -t <device-id> file recv /data/local/tmp/dual_screen-single-pane.jpeg ./blog-assets/dual_screen/single-pane.jpeg

一、插件简介与适配目标

双区域布局与折叠屏传感信息是两个相互配合的能力。TwoPane 在 Dart 侧排列两个区域,DualScreenInfo 在 OHOS 侧使用 display.isFoldable 和 foldAngleChange 获取设备能力与角度。

例如,邮件应用可以将列表与详情并排,编辑器可以根据可用空间改为上下布局,折叠设备上的交互也可以参考铰链角度。

是否折叠通过一次查询获取,角度通过 EventChannel 持续推送。TwoPane 自动避让物理折痕还依赖 Flutter 引擎提供 displayFeatures,不能由角度事件自行推断折痕矩形。


二、环境准备

环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。

完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:

bash 复制代码
flutter --version
flutter doctor -v
hdc list targets

工程使用的工具链和 SDK 配置如下:

项目 版本或配置 用途
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) 开发套件版本及对应的 API 级别
compileSdkVersion 26.0.0 编译时使用的 SDK API
targetSdkVersion 26.0.0 应用面向的行为版本
compatibleSdkVersion 18 当前工程声明的最低兼容版本
插件版本 1.0.4 pubspec.yaml 中的包版本
原生语言 ArkTS HarmonyOS 插件实现
插件产物 HAR 被应用 entry 模块依赖

2.1 开发套件版本与工程中的 SDK 版本配置

7.0.0(API 26)26.0.0 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:

  • 7.0.0(API 26) 表示 HarmonyOS 开发套件版本为 7.0.0,对应 API 26。
  • 26.0.0 是本文 HarmonyOS 应用工程中 compileSdkVersiontargetSdkVersion 的属性值。
  • 18 是本文工程中 compatibleSdkVersion 的属性值,声明最低兼容 API 18。

本地示例使用 OpenHarmony product,compatibleSdkVersion 为数字 18runtimeOSOpenHarmony。对应的 product 配置为:

json5 复制代码
{
  "name": "default",
  "compatibleSdkVersion": 18,
  "compileSdkVersion": "26.0.0",
  "targetSdkVersion": "26.0.0",
  "runtimeOS": "OpenHarmony"
}

这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 18。普通双区域布局、可折叠能力和角度事件可以分别使用。当前项目说明指出 OHOS 引擎尚未提供可靠的物理折痕 displayFeatures,TwoPane 自动贴合折痕不能保证;双轴折叠仅返回系统角度数组的第一项。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到 AtomGit

适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yamlLICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

上游源码为 https://github.com/microsoft/flutter-dualscreen,本文基于 1.0.4。配套仓库预定为 dual_screen。仓库创建并同步适配代码后,再执行下面的拉取命令;尚未同步时使用本地副本。需要提交修改时,使用自己有写权限的仓库或 Fork。

3.2 将代码拉取到宿主机

在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:

bash 复制代码
git clone https://atomgit.com/oh-flutter/flutter-dualscreen.git
cd flutter-dualscreen
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

git clone 会创建 flutter-dualscreen/ 目录;cd 后的位置就是下文所说的插件仓库根目录 ,这里应能看到 pubspec.yamllib/example/。Git 仓库名为 flutter-dualscreen,Dart 包名为 dual_screen

需要使用与本文相同的代码版本时,先将适配工作区整理、提交并同步到配套仓库,再将下方占位符替换为实际适配提交号。在没有未提交修改的仓库中执行:

bash 复制代码
git switch --detach <适配提交号>

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入 AtomGit 仓库拉取命令。

3.3 在仓库根目录创建适配分支

接着在 dual_screen/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yamlname,版本号取此次适配的基线版本。本例为:

bash 复制代码
git switch -c feat/ohos_dual_screen_1.0.4
git branch --show-current

如果该分支已存在,使用 git switch feat/ohos_dual_screen_1.0.4 切换即可。

图 2:在 dual_screen 仓库根目录输入适配分支创建命令。

3.4 自动补全 OHOS 适配结构

分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件

bash 复制代码
flutter create --template=plugin --platforms=ohos --project-name dual_screen .
git status --short
git diff -- pubspec.yaml lib example
  • --template=plugin 指定插件模板。
  • --platforms=ohos 指定需要补全的平台。
  • --project-name dual_screen 使用 Dart 包名,避免当前目录重命名后生成错误的包名。
  • 最后的 . 表示在当前插件目录补全工程,不是另建一层 dual_screen/

该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yamllib/example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。

如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:

bash 复制代码
cd example
flutter create --platforms=ohos .
cd ..

本地适配工作区已经包含 ohos/example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos dual_screen;已有插件使用上面的 . 在当前目录补全。

图 3:在插件根目录输入 OHOS 结构补全命令。

3.5 适配后的项目目录

适配后的关键目录如下:

text 复制代码
dual_screen/
├── lib/
│   ├── dual_screen.dart
│   └── src/
│       ├── dual_screen_info.dart
│       ├── two_pane.dart
│       └── media_query_extension.dart
├── ohos/
│   ├── src/
│   │   └── main/
│   │       ├── ets/
│   │       │   └── components/
│   │       │       └── plugin/
│   │       │           └── DualScreenInfo.ets
│   │       └── module.json5
│   ├── index.ets
│   └── oh-package.json5
├── example/
│   ├── lib/
│   │   └── main.dart
│   └── ohos/
│       ├── entry/
│       │   └── src/
│       │       └── main/
│       │           └── module.json5
│       └── build-profile.json5
├── pubspec.yaml
└── test/

项目根目录如下,其中包含 ohos/example/ 及实际保留的说明文件;交付文档清单见第六节:

图 4:适配后的 dual_screen 项目根目录。

文件 主要职责
lib/dual_screen.dart 提供业务公开 API
lib/src/dual_screen_info.dart 实现平台协议或数据模型
lib/src/two_pane.dart 实现平台协议或数据模型
lib/src/media_query_extension.dart 实现平台协议或数据模型
ohos/src/main/ets/components/plugin/DualScreenInfo.ets 注册通道并实现 OHOS 原生能力
ohos/src/main/module.json5 声明 HAR 模块和权限
example/lib/main.dart 演示接口调用与结果显示

四、Dart 接口与通道分析

OHOS 实现需要遵循 Dart 层已有的方法、参数和回调约定。先阅读 lib/dual_screen.dartlib/src/dual_screen_info.dartlib/src/two_pane.dartlib/src/media_query_extension.dart,再在 ohos/src/main/ets/components/plugin/DualScreenInfo.ets 中实现对应的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型 通道协议 OHOS 实现 应保持的行为
TwoPane(...) 纯 Dart 布局 Flutter Render/Layout 左右、上下、单区域与比例
hasHingeAngleSensor hinge_info / hasHingeAngleSensor display.isFoldable() 折叠能力近似判断
hingeAngleEvents hinge_angle 事件通道 foldAngleChange double;双轴取第一项
MediaQueryData.hinge 引擎 displayFeatures 查找 hinge 类型 缺失则为 null

原生端需要保持方法名、参数键和返回类型一致,不能只保留方法名称而改变业务语义。

4.1 跨端架构与调用时序

Flutter 侧通过 MethodChannel('com.microsoft.flutterdualscreen/hinge_info') 调用 OHOS 实现。持续变化通过 EventChannel('com.microsoft.flutterdualscreen/hinge_angle') 推送;一次查询结果与后续事件分别处理。
#mermaid-svg-VURugDCXwbwsIn5p{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VURugDCXwbwsIn5p .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VURugDCXwbwsIn5p .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VURugDCXwbwsIn5p .error-icon{fill:#552222;}#mermaid-svg-VURugDCXwbwsIn5p .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VURugDCXwbwsIn5p .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VURugDCXwbwsIn5p .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VURugDCXwbwsIn5p .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VURugDCXwbwsIn5p .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VURugDCXwbwsIn5p .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VURugDCXwbwsIn5p .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VURugDCXwbwsIn5p .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VURugDCXwbwsIn5p .marker.cross{stroke:#333333;}#mermaid-svg-VURugDCXwbwsIn5p svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VURugDCXwbwsIn5p p{margin:0;}#mermaid-svg-VURugDCXwbwsIn5p .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VURugDCXwbwsIn5p .cluster-label text{fill:#333;}#mermaid-svg-VURugDCXwbwsIn5p .cluster-label span{color:#333;}#mermaid-svg-VURugDCXwbwsIn5p .cluster-label span p{background-color:transparent;}#mermaid-svg-VURugDCXwbwsIn5p .label text,#mermaid-svg-VURugDCXwbwsIn5p span{fill:#333;color:#333;}#mermaid-svg-VURugDCXwbwsIn5p .node rect,#mermaid-svg-VURugDCXwbwsIn5p .node circle,#mermaid-svg-VURugDCXwbwsIn5p .node ellipse,#mermaid-svg-VURugDCXwbwsIn5p .node polygon,#mermaid-svg-VURugDCXwbwsIn5p .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VURugDCXwbwsIn5p .rough-node .label text,#mermaid-svg-VURugDCXwbwsIn5p .node .label text,#mermaid-svg-VURugDCXwbwsIn5p .image-shape .label,#mermaid-svg-VURugDCXwbwsIn5p .icon-shape .label{text-anchor:middle;}#mermaid-svg-VURugDCXwbwsIn5p .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VURugDCXwbwsIn5p .rough-node .label,#mermaid-svg-VURugDCXwbwsIn5p .node .label,#mermaid-svg-VURugDCXwbwsIn5p .image-shape .label,#mermaid-svg-VURugDCXwbwsIn5p .icon-shape .label{text-align:center;}#mermaid-svg-VURugDCXwbwsIn5p .node.clickable{cursor:pointer;}#mermaid-svg-VURugDCXwbwsIn5p .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VURugDCXwbwsIn5p .arrowheadPath{fill:#333333;}#mermaid-svg-VURugDCXwbwsIn5p .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VURugDCXwbwsIn5p .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VURugDCXwbwsIn5p .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VURugDCXwbwsIn5p .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VURugDCXwbwsIn5p .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VURugDCXwbwsIn5p .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VURugDCXwbwsIn5p .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VURugDCXwbwsIn5p .cluster text{fill:#333;}#mermaid-svg-VURugDCXwbwsIn5p .cluster span{color:#333;}#mermaid-svg-VURugDCXwbwsIn5p div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VURugDCXwbwsIn5p .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VURugDCXwbwsIn5p rect.text{fill:none;stroke-width:0;}#mermaid-svg-VURugDCXwbwsIn5p .icon-shape,#mermaid-svg-VURugDCXwbwsIn5p .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VURugDCXwbwsIn5p .icon-shape p,#mermaid-svg-VURugDCXwbwsIn5p .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VURugDCXwbwsIn5p .icon-shape .label rect,#mermaid-svg-VURugDCXwbwsIn5p .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VURugDCXwbwsIn5p .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VURugDCXwbwsIn5p .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VURugDCXwbwsIn5p :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Flutter 页面
Dart 对外 API
MethodChannel com.microsoft.flutterdualscreen/hinge_info
ArkTS DualScreenInfo
ArkUI display
EventChannel 与方法结果

4.1.1 一次完整查询折叠能力的时序

display OHOS 插件 DualScreenInfo Flutter 页面 display OHOS 插件 DualScreenInfo Flutter 页面 #mermaid-svg-uioPdJV9DD2F2Zr5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uioPdJV9DD2F2Zr5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uioPdJV9DD2F2Zr5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uioPdJV9DD2F2Zr5 .error-icon{fill:#552222;}#mermaid-svg-uioPdJV9DD2F2Zr5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uioPdJV9DD2F2Zr5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uioPdJV9DD2F2Zr5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uioPdJV9DD2F2Zr5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uioPdJV9DD2F2Zr5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uioPdJV9DD2F2Zr5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uioPdJV9DD2F2Zr5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uioPdJV9DD2F2Zr5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uioPdJV9DD2F2Zr5 .marker.cross{stroke:#333333;}#mermaid-svg-uioPdJV9DD2F2Zr5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uioPdJV9DD2F2Zr5 p{margin:0;}#mermaid-svg-uioPdJV9DD2F2Zr5 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uioPdJV9DD2F2Zr5 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-uioPdJV9DD2F2Zr5 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uioPdJV9DD2F2Zr5 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-uioPdJV9DD2F2Zr5 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-uioPdJV9DD2F2Zr5 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-uioPdJV9DD2F2Zr5 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-uioPdJV9DD2F2Zr5 .sequenceNumber{fill:white;}#mermaid-svg-uioPdJV9DD2F2Zr5 #sequencenumber{fill:#333;}#mermaid-svg-uioPdJV9DD2F2Zr5 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-uioPdJV9DD2F2Zr5 .messageText{fill:#333;stroke:none;}#mermaid-svg-uioPdJV9DD2F2Zr5 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uioPdJV9DD2F2Zr5 .labelText,#mermaid-svg-uioPdJV9DD2F2Zr5 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-uioPdJV9DD2F2Zr5 .loopText,#mermaid-svg-uioPdJV9DD2F2Zr5 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-uioPdJV9DD2F2Zr5 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uioPdJV9DD2F2Zr5 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-uioPdJV9DD2F2Zr5 .noteText,#mermaid-svg-uioPdJV9DD2F2Zr5 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-uioPdJV9DD2F2Zr5 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uioPdJV9DD2F2Zr5 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uioPdJV9DD2F2Zr5 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uioPdJV9DD2F2Zr5 .actorPopupMenu{position:absolute;}#mermaid-svg-uioPdJV9DD2F2Zr5 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-uioPdJV9DD2F2Zr5 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uioPdJV9DD2F2Zr5 .actor-man circle,#mermaid-svg-uioPdJV9DD2F2Zr5 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-uioPdJV9DD2F2Zr5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 非折叠设备 折叠设备 当前重放封装仍持有底层订阅 hasHingeAngleSensor hasHingeAngleSensor isFoldable() bool 能力 hingeAngleEvents.listen EventChannel onListen endOfStream on(foldAngleChange) angles angles0 double 角度 取消页面订阅 Engine 解绑时 off(foldAngleChange)

4.2 布局、折痕与角度模型

数据 语义
TwoPanePriority.start/both/end 显示主区域、双区域或副区域
Axis.horizontal/vertical 左右或上下排列
paneProportion 非分隔显示特征场景的空间比例
double 角度 首个折叠轴的角度值
DisplayFeature? hinge 来自 MediaQuery,可能为空
dart 复制代码
/// Copyright (c) Microsoft Corporation.
/// Licensed under the MIT License.

import 'dart:ui';
import 'package:flutter/widgets.dart';

/// Extension method that helps with working with the hinge specifically.
extension MediaQueryHinge on MediaQueryData {
  DisplayFeature? get hinge {
    for (final DisplayFeature e in displayFeatures) {
      if (e.type == DisplayFeatureType.hinge) {
        return e;
      }
    }
    return null;
  }
}

角度事件按 num.toDouble 转换,不能把三张布局截图当成三种真实铰链角度。

4.3 公开 API 与平台接口

TwoPane 保持主副区域、方向、比例与 panePriority 参数;DualScreenInfo 的静态属性提供原生信息。能力查询异常会返回 false:

dart 复制代码
static Future<bool> get hasHingeAngleSensor async {
  try {
    return await _hingeInfoMethodChannel.invokeMethod<bool>(
          'hasHingeAngleSensor',
        ) ??
        false;
  } catch (e) {
    return false;
  }
}

4.4 Dart 通道协议分析

4.4.1 通道名称必须两端完全一致
dart 复制代码
const methodChannel = MethodChannel('com.microsoft.flutterdualscreen/hinge_info');
const eventChannel = EventChannel('com.microsoft.flutterdualscreen/hinge_angle');

通道名称属于跨语言协议。Dart 和 ArkTS 任何一端拼写不一致,都会出现"方法未实现"或"调用找不到插件"等问题。

4.4.2 监听铰链角度
dart 复制代码
static Stream<double> get hingeAngleEvents {
  try {
    if (_hingeAngleEvents == null) {
      _hingeAngleEvents = _repeatLatest(
        _hingeAngleEventChannel.receiveBroadcastStream().map(
          (event) => (event as num).toDouble(),
        ),
      );
    }
    return _hingeAngleEvents!;
  } catch (e) {
    return Stream.empty();
  }
}

_repeatLatest 缓存并重放最近一个角度。外层 try/catch 只捕获同步创建错误,异步事件流错误仍要通过 onError 处理。

4.4.3 缓存重放与监听生命周期
dart 复制代码
static Stream<T> _repeatLatest<T>(Stream<T> original) {
  var done = false;
  T? latest;
  var currentListeners = <MultiStreamController<T>>{};
  original.listen(
    (event) {
      latest = event;
      for (var listener in [...currentListeners]) listener.addSync(event);
    },
    onError: (Object error, StackTrace stack) {
      for (var listener in [...currentListeners])
        listener.addErrorSync(error, stack);
    },
    onDone: () {
      done = true;
      latest = null;
      for (var listener in currentListeners) listener.closeSync();
      currentListeners.clear();
    },
  );
  return Stream.multi((controller) {
    if (done) {
      controller.close();
      return;
    }
    currentListeners.add(controller);
    var latestValue = latest;
    if (latestValue != null) controller.add(latestValue);
    controller.onCancel = () {
      if (!done) {
        currentListeners.remove(controller);
      }
    };
  });
}

当前实现立即订阅 original,但没有保留并在最后一个业务监听取消时解除底层订阅。取消页面监听只会移除当前 controller,不能保证原生 foldAngleChange 已停止;Engine 解绑仍会执行原生清理。


五、补全 OHOS 原生实现与工程配置

5.1 在 DualScreenInfo.ets 中实现原生能力

业务层沿用已有 API,原生侧在 DualScreenInfo 中接入 ArkUI display,通过 Flutter 通道回传结果。

下面列出类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口;各段均为核心摘录,需要结合完整类使用。

原生插件位于:

text 复制代码
ohos/src/main/ets/components/plugin/DualScreenInfo.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
typescript 复制代码
import {
  EventChannel,
  EventSink,
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import { display } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

FlutterPlugin 负责接入 Flutter Engine 生命周期,MethodChannel 接收 Dart 命令;系统能力由 ArkUI display 提供。错误和事件处理以对应方法实现为准。

5.1.2 连接 Flutter Engine
typescript 复制代码
private hingeAngleChannel: EventChannel | null = null;
private hingeInfoChannel: MethodChannel | null = null;
private hingeAngleSink: EventSink | null = null;
private isListening: boolean = false;

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

  // The Dart API exposes one angle. On dual-fold devices, use the first
  // axis, which HarmonyOS defines as the rightmost hinge.
  this.hingeAngleSink?.success(angles[0]);
};


onAttachedToEngine(binding: FlutterPluginBinding): void {
  const messenger = binding.getBinaryMessenger();

  this.hingeAngleChannel = new EventChannel(messenger, HINGE_ANGLE_CHANNEL_NAME);
  this.hingeAngleChannel.setStreamHandler({
    onListen: (_arguments: Object, events: EventSink): void => {
      this.startListening(events);
    },
    onCancel: (_arguments: Object): void => {
      this.stopListening();
    },
  });

  this.hingeInfoChannel = new MethodChannel(messenger, HINGE_INFO_CHANNEL_NAME);
  this.hingeInfoChannel.setMethodCallHandler(this);
}

Engine 创建 hinge_info 查询通道与 hinge_angle 事件通道。TwoPane 布局本身不通过这些通道,显示特征由 Flutter MediaQuery 提供。

5.1.3 接收折叠角度
typescript 复制代码
private startListening(events: EventSink): void {
  this.stopListening();

  try {
    if (!display.isFoldable()) {
      events.endOfStream();
      return;
    }

    this.hingeAngleSink = events;
    display.on('foldAngleChange', this.foldAngleCallback);
    this.isListening = true;
  } catch (error) {
    const businessError = error as BusinessError;
    this.hingeAngleSink = null;
    hilog.error(
      0,
      TAG,
      `Failed to listen for fold angle changes: ${businessError.code} ${businessError.message}`,
    );
    events.error(
      String(businessError.code ?? 'DISPLAY_ERROR'),
      businessError.message ?? 'Failed to listen for fold angle changes.',
      null,
    );
  }
}

非折叠设备发送 endOfStream;折叠设备注册 foldAngleChange。回调数组为空时忽略,否则发送 angles0。没有主动查询当前角度,所以初始时可能一直等待下一次变化。

5.1.4 取消系统角度监听
typescript 复制代码
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;
}

原生 onCancel 或 Engine 解绑解除原回调并清空 sink。由于 Dart 的重放封装保留底层订阅,页面取消与原生 onCancel 并不是必然一一对应。

5.1.5 处理 MethodChannel 命令
typescript 复制代码
onMethodCall(call: MethodCall, result: MethodResult): void {
  if (call.method !== 'hasHingeAngleSensor') {
    result.notImplemented();
    return;
  }

  try {
    result.success(display.isFoldable());
  } catch (error) {
    const businessError = error as BusinessError;
    hilog.error(
      0,
      TAG,
      `Failed to query foldable capability: ${businessError.code} ${businessError.message}`,
    );
    result.error(
      String(businessError.code ?? 'DISPLAY_ERROR'),
      businessError.message ?? 'Failed to query foldable capability.',
      null,
    );
  }
}

未知方法返回 notImplemented。原生查询异常返回错误码或 DISPLAY_ERROR,Dart hasHingeAngleSensor 捕获后返回 false;监听注册异常走 EventSink.error。设备无折叠能力时正常结束事件流,不产生虚构角度。

5.1.6 Engine 解绑时释放资源
typescript 复制代码
onDetachedFromEngine(_binding: FlutterPluginBinding): void {
  this.stopListening();
  this.hingeAngleChannel?.setStreamHandler(null);
  this.hingeAngleChannel = null;
  this.hingeInfoChannel?.setMethodCallHandler(null);
  this.hingeInfoChannel = null;
}

Engine 解绑调用 stopListening,释放事件 Handler 与方法 Handler;对页面取消无法关闭的底层重放订阅,这是原生资源最终清理边界。

5.2 声明插件和宿主权限

折叠能力查询和 foldAngleChange 不需要新增敏感权限。HAR 权限为空,example 保留 INTERNET。

5.2.1 插件 HAR 的权限

插件 ohos/src/main/module.json5 的模块配置如下:

json5 复制代码
{
  "module": {
    "name": "dual_screen",
    "type": "har",
    "deviceTypes": [
      "default",
      "tablet"
    ]
  }
}
5.2.2 应用 entry 的权限

最终安装的是宿主应用。以下片段来自 example/ohos/entry/src/main/module.json5,合并时保留原有 Ability 等配置:

json5 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

权限声明与运行时授权需要分别处理;没有权限需求的功能不要套用其他插件的授权流程。

5.3 注册并导出插件

pubspec.yaml 通过以下配置声明 OHOS 插件类:

yaml 复制代码
flutter:
  plugin:
    platforms:
      ohos:
        pluginClass: DualScreenInfo

插件的 ohos/index.ets 需要导出实现:

typescript 复制代码
import DualScreenInfo from './src/main/ets/components/plugin/DualScreenInfo';

export default DualScreenInfo;

执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。

注册异常的排查步骤见第九节 MissingPluginException

5.4 检查 example 的 OHOS 应用结构

本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地。

json5 复制代码
{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": 18,
        "runtimeOS": "OpenHarmony",
        "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 模块的签名配置。


六、补全交付文件并提交适配分支

6.1 除代码外还要补全哪些文件

代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:

文件 应写清楚的内容
README.OpenSource 上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖
README.md 原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息
README.OpenHarmony_CN.md 简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题
README.OpenHarmony.md 与中文说明对应的英文文档
CHANGELOG.OpenHarmony.md OHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE / NOTICE 保留上游许可证;NOTICE 按许可证和原项目要求保留或补充
example/README.md 依赖方式、运行目录、签名、操作步骤与效果图;覆盖主副布局、比例调整、折叠能力与角度事件
pubspec.yamlohos/oh-package.json5 核对包名、版本、插件注册、仓库地址、许可证和依赖
.gitignore 忽略构建缓存及本机签名材料,不漏提交必要源码和配置

README.OpenSource 记录库本身的来源与版本。本例的包名为 dual_screen,版本为 1.0.4,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。

当前本地 README.mdCHANGELOG.md 和 example/README.md 已记录 OHOS 内容,专用 OpenHarmony 中英文说明与 README.OpenSource 尚需按交付要求补齐。上游沿用 MIT 许可证。

6.2 提交前检查

提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:

bash 复制代码
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff

检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。

6.3 提交并推送到 AtomGit

文档和代码整理完成后,在 Git 仓库根目录暂存并提交。以下命令以第 6.1 节文档已经补全为前提,文件名按项目实际情况调整:

bash 复制代码
git add lib ohos pubspec.yaml example test
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for dual_screen 1.0.4"
git remote -v
git branch --show-current
git push -u origin feat/ohos_dual_screen_1.0.4

DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己的 AtomGit 仓库,当前分支为 feat/ohos_dual_screen_1.0.4

推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。


七、使用根目录 example 演示接入

插件包自带 example/,可以直接用来调试插件和体验双区域布局与铰链角度。

7.1 本地适配时使用路径依赖

当前 example/pubspec.yaml 的依赖是:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  dual_screen:
    path: ../

../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。

7.2 通过 AtomGit 引入插件

业务应用通过 AtomGit 引入时,将 dual_screenpath 配置替换为下面的 Git 依赖。仓库同步后,将 ref 替换为实际适配提交号:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  dual_screen:
    git:
      url: https://atomgit.com/oh-flutter/flutter-dualscreen.git
      ref: <适配提交号>

使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_dual_screen_1.0.4。正式发布后可固定到 tag 或 commit。

从插件根目录执行:

bash 复制代码
cd example
flutter pub get
flutter pub deps

检查 example/pubspec.lockdual_screen 的来源为 git,并核对 urlrefresolved-ref。同时检查没有 dependency_overridespubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。

7.3 调用接口实现双区域布局与铰链角度

下面的页面可用于插件包内的 example/lib/main.dart,是便于讲解的最小页面;仓库完整 Demo 的入口和布局可能不同,第八节截图与验收步骤以仓库完整 Demo 为准。

最小页面关闭 displayFeatures 对布局参数的自动覆盖,仅展示普通双区域排列与角度读取;物理折痕效果以完整 Demo 和真实引擎数据另行验证。

dart 复制代码
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:dual_screen/dual_screen.dart';


void main() {
  runApp(const MaterialApp(home: DemoPage()));
}

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});
  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  String _status = '尚未操作';
  bool _busy = false;
  StreamSubscription<double>? _angles;

  @override
  void initState() {
    super.initState();
    _angles = DualScreenInfo.hingeAngleEvents.listen(
      (value) => _show('铰链角度:$value'),
      onError: (Object error) => _show('角度错误:$error'),
    );
  }

  void _show(String value) {
    if (mounted) setState(() => _status = value);
  }

  Future<void> _run(Future<String> Function() action) async {
    if (_busy) return;
    setState(() => _busy = true);
    try {
      _show(await action());
    } catch (error) {
      _show('调用失败:$error');
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  @override
  void dispose() {
    unawaited(_angles?.cancel());
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('双区域布局')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text(_status),
          const SizedBox(height: 16),
            FilledButton(onPressed: _busy ? null : () => _run(() async {
              return '可折叠:${await DualScreenInfo.hasHingeAngleSensor}';
            }), child: const Text('查询折叠能力')),
          SizedBox(
            height: 320,
            child: TwoPane(
              startPane: Container(color: Colors.green.shade50, child: const Center(child: Text('主区域'))),
              endPane: Container(color: Colors.blue.shade50, child: const Center(child: Text('副区域'))),
              panePriority: TwoPanePriority.both,
              direction: Axis.horizontal,
              allowedOverrides: const {},
            ),
          ),
        ],
      ),
    );
  }
}

7.4 页面退出时取消页面角度监听

异步回调先检查 mounted,dispose 中取消页面 StreamSubscription。当前 _repeatLatest 不会自动取消 original,因此不要宣称页面退出后一定停止了系统角度监听。多个页面使用时,建议由应用级服务管理状态分发;底层清理边界需与实际实现一致。


八、验证、构建与鸿蒙设备运行效果

8.1 分别验证插件与 example

从插件仓库根目录执行:

bash 复制代码
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test

现有测试覆盖能力查询、角度事件、无传感器场景、MediaQuery.hinge 和 TwoPane 方向/比例/优先级布局。测试注入的 displayFeatures 不能证明真实 OHOS 引擎已提供物理折痕。Widget 测试应匹配实际保留的 Demo 页面;如果替换成第七节最小页面,也要相应调整原来的 UI 断言。

Dart 测试覆盖接口和页面逻辑,主副布局、比例调整、折叠能力与角度事件还需要在鸿蒙设备上验证。以上为 Dart 测试复现命令,本次未重新执行这些测试;本次已重新构建、安装并运行签名 Release HAP,具体真机采集范围见 8.5。

8.2 确认设备连接

bash 复制代码
hdc list targets
flutter devices

设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。

8.3 配置签名

真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs
  4. default product 选择或生成签名;
  5. 确认设备、应用包名、证书和 Profile 匹配;
  6. 再回到终端执行 Flutter 构建或运行。

签名材料保存在本机,公开仓库中只保留构建所需的通用配置。

8.4 运行示例

以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:

bash 复制代码
flutter run -d <device-id>

也可以先构建 HAP:

bash 复制代码
flutter build hap --debug

典型产物位于:

text 复制代码
example/ohos/entry/build/default/outputs/default/

目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。

本次真机截图使用签名 Release HAP。在插件包的 example/ 目录完成签名配置后执行:

bash 复制代码
flutter build hap --release
hdc -t <device-id> install -r build/ohos/hap/entry-default-signed.hap
hdc -t <device-id> shell aa start -a EntryAbility -b com.microsoft.flutterdualscreen.dual_screen_example

8.5 在设备上测试双区域布局与铰链角度

  1. 运行完整 Demo,确认标题为"dual_screen 鸿蒙示例",查看设备能力与角度区域。
  2. 选择"双区域"和"左右",观察主、副区域并排。
  3. 切换"上下",拖动"比例",核对布局变化。
  4. 分别选择"主区域"和"副区域",确认单区域显示。
  5. 在实际折叠设备上改变铰链角度,检查事件;非折叠设备应显示无能力而不是伪造角度。
  6. 检查 MediaQuery 中是否存在真实 displayFeatures,再评估自动避让物理折痕;布局截图与硬件角度分开验收。

页面初始提示来自 Demo 默认值,调用结果或事件到达后才反映系统状态。上述列表为完整验收步骤,本次实际采集范围如下。

8.6 鸿蒙设备运行效果

OHOS 实现提供双区域布局与铰链角度。以下为仓库完整 Demo 的三张真机运行截图,按实际状态记录。

真机运行图(从左到右):左右布局/上下布局/单区域。采集于 2026-09-09,设备 ALN-AL00 / HUAWEI Mate 60 Pro,系统 OpenHarmony 6.1.1.120(API 24);使用本地当前源码构建的签名 Release HAP。

本机是非折叠手机,三图展示布局切换,不作为铰链角度的验证证据。
截图标注:操作步骤与截图命令

执行目录:本文 Markdown 所在目录。先用 hdc list targets 获取设备 ID,将下列 <device-id> 替换为实际值。截图使用仓库完整 Demo;snapshot_display 在本机要求 .jpeg 后缀,图片保持原始真机画面。

bash 复制代码
mkdir -p blog-assets/dual_screen

左右布局: 选择"双区域"和"左右",保留主、副区域并排布局。

bash 复制代码
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/dual_screen-horizontal.jpeg
hdc -t <device-id> file recv /data/local/tmp/dual_screen-horizontal.jpeg ./blog-assets/dual_screen/horizontal.jpeg

上下布局: 保持"双区域",切换"上下",比例保持 50%。

bash 复制代码
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/dual_screen-vertical.jpeg
hdc -t <device-id> file recv /data/local/tmp/dual_screen-vertical.jpeg ./blog-assets/dual_screen/vertical.jpeg

单区域: 选择"主区域",查看单一内容区。

bash 复制代码
hdc -t <device-id> shell snapshot_display -f /data/local/tmp/dual_screen-single-pane.jpeg
hdc -t <device-id> file recv /data/local/tmp/dual_screen-single-pane.jpeg ./blog-assets/dual_screen/single-pane.jpeg
左右布局 上下布局 单区域
普通左右布局 普通上下布局 优先级布局

普通双区域布局、可折叠能力和角度事件可以分别使用。当前项目说明指出 OHOS 引擎尚未提供可靠的物理折痕 displayFeatures,TwoPane 自动贴合折痕不能保证;双轴折叠仅返回系统角度数组的第一项。


九、FAQ:适配过程与使用问题

9.1 Missing SDK components

典型错误如下:

text 复制代码
Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.

这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录;
  4. 确认 SDK 根目录下存在 toolchainsetsjsnativepreviewer
  5. 执行 flutter config --ohos-sdk <正确路径>
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。

为什么连接 API 24 手机仍然会报这个错误?

因为 Sync 和 Compile 首先读取 Mac 本地 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。

当前工程的 compatibleSdkVersion 是 API 18,因此 API 24 在安装版本门槛上是满足的;但功能是否可用还取决于目标系统能力、权限和运行环境,不能仅凭最低版本判断。

9.2 DevEco Studio 中看不到 entry 模块

插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry

请直接使用 DevEco Studio 打开:

text 复制代码
dual_screen/example/ohos

如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。

9.3 无法手动签名

签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。

建议先确认:

  • 打开的是 example/ohos
  • SDK 组件完整并且 Sync 成功;
  • entry 的模块类型为 entry
  • default product 和 target 已正确关联;
  • 当前账号、证书和调试设备状态有效。

9.4 显示可折叠,但一直没有角度

hasHingeAngleSensor 在 OHOS 实际使用 display.isFoldable。角度注册后没有主动发送初始值,需要系统产生 foldAngleChange;同时检查事件错误和设备支持。可折叠标记不保证立刻拿到角度样本。

9.5 MissingPluginException

这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:

bash 复制代码
cd example
flutter clean
flutter pub get
flutter run -d <device-id>

如果仍然出现,检查自动生成的插件注册文件中是否包含 DualScreenInfo,同时核对 pubspec.yamlohos/index.etsoh-package.json5

9.6 页面取消后为什么系统仍可能监听

_repeatLatest 创建并保留原始流监听,业务 controller 取消只从集合移除自身,没有取消 original。不能套用"最后一个页面退出必然停止原生监听"的说法;Engine 解绑会执行 stopListening。

9.7 编译成功但安装失败

常见原因包括:

  • HAP 未签名或使用了错误的 Profile;
  • 设备未加入调试设备列表;
  • 包名与签名 Profile 不匹配;
  • 安装包的 compatibleSdkVersion 高于设备 API;
  • 手机上已经安装了使用不同证书签名的同包名应用。

根据安装错误码区分签名、版本和包名冲突,再处理对应配置。

9.8 flutter create 不认识 ohos,或包名不合法

先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name dual_screen;本例仓库名和 Dart 包名均为 dual_screen。生成后检查 diff,再补充 ArkTS 业务实现。

9.9 AtomGit 依赖提示找不到分支或无权限

先检查 URL 是否指向已同步的目标仓库,再确认 feat/ohos_dual_screen_1.0.4 已推送。仓库未创建、适配分支未推送或提交未同步时,应先完成同步;不能直接使用仅存在本地的提交号。私有仓库还需在本机配置 Git 认证。

9.10 改了本地 ArkTS,Demo 为什么没变化

先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lockresolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。

9.11 TwoPane 为什么没有沿物理折痕分开

自动避让依赖 MediaQuery.displayFeatures 中有效的分隔区域,角度本身不提供矩形。当前 OHOS 引擎的折痕数据存在边界,普通左右/上下布局生效并不证明物理折痕适配已完成。


相关链接

相关推荐
熊猫钓鱼>_>3 小时前
【SenseNova U1.5 Lite实战】鸿蒙校园工具开发者适配原生统一多模态大模型全记录
人工智能·华为·ai·harmonyos·媒体·sensenova
在人间耕耘4 小时前
鸿蒙7「互动卡片」实测:桌面上多了个“记一笔“按钮 快的很
华为·harmonyos
昇腾知识体系4 小时前
昇腾 AscendC Tiling 设计实战:TilingFunc/TilingData 完整示例与 UB 容量预算
人工智能·华为·知识图谱
RUNIONE4 小时前
合亿RUNIONE 三防平板电脑|手持工业平板 UA810 开源鸿蒙系统,与国产生态深度融合
harmonyos·手持工业平板·手持工业平板厂家
●VON5 小时前
鸿蒙跨平台框架怎么选?从真实需求比较 Flutter、React Native、KMP/CMP 与 Web 路线
flutter·react native·harmonyos
lqj_本人6 小时前
Flutter 三方库「flutter_displaymode」的鸿蒙化适配指南
flutter·华为·harmonyos
邓孟鑫7 小时前
鸿蒙图表MarkerView遮挡解决方案
华为·harmonyos
梦想不只是梦与想7 小时前
鸿蒙 指定设备发布:内部测试
harmonyos·内部测试·指定设备发布
lqj_本人8 小时前
Flutter 三方库「drag_and_drop_flutter」的鸿蒙化适配指南
flutter·华为·harmonyos