从 0 到 1 写一个 Flutter OHOS 插件
ohos_immersive_light(HDS TabBar 沉浸光感),把一路上踩过的坑一次性总结给你。
本文侧重Windows 平台特有的坑——macOS用户可以略过第三章,但通用部分(PlatformView、Builder 限制、HashMap 解码、AbilityAware)请务必看。
一、为什么写ohos_immersive_light
- 目标:封装 OpenHarmony UIDesignKit 的
HdsTabs,给 Flutter 提供悬浮样式 + 沉浸光感效果的 TabBar。 - 能力:
- 用
OhosView嵌入原生HdsTabs组件 - 支持
phone/tablet/2in1设备的悬浮样式(API 23+),其他设备自动回退 - 支持
selectedColor/unselectedColor/ 自定义 Tab 列表 - Tab 切换事件实时回传 Dart 侧
- 通过 MethodChannel 暴露
isFloatingSupported()给业务方做门控
- 用
底层机制就是Dart 侧OhosView+ MethodChannel,OHOS 侧FlutterPlugin+PlatformViewFactory+@Builder+ 原生组件。把这一套打通,换个原生组件就能复刻出第二个、第三个插件。
二、通用开发流程(跨平台)
2.1 Dart 层
- 定义
ImmersiveLightOptions/ImmersiveLightTabItem配置类,配套toMap()把字段转成原生可识别的基本类型(String/bool/num/List/Map)。 - 入口类
OhosImmersiveLight提供getPlatformVersion()/isFloatingSupported()等方法,走MethodChannel。 - 视图类
ImmersiveLightView extends StatefulWidget,内部用OhosView,viewType: 'immersive_light_view',creationParams: options.toMap(),并在onPlatformViewCreated里给独立 MethodChannel 设setMethodCallHandler,接收onTabChange等原生回调。
2.2 OHOS 层
OhosImmersiveLightPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware。onAttachedToEngine里:注册主MethodChannel+ 注册PlatformViewFactory(getPlatformViewRegistry().registerViewFactory(viewType, factory))。onAttachedToAbility里:保存AbilityPluginBinding,需要UIAbilityContext时从这里拿(本插件用不到,但写上不亏)。ImmersiveLightViewFactory extends PlatformViewFactory,构造里super(StandardMessageCodec.INSTANCE)。ImmersiveLightPlatformView extends PlatformView,getView()返回一个WrappedBuilder,包住@Builder函数。@Builder函数签名是($$: Params),里面只能写 UI 组件语法,不能let、不能const。@Component内部组件从params.platformView拿到业务对象(强转),把字段赋给原生组件;事件通过查viewChannelMap找到对应 MethodChannel,回调到 Dart。
跨平台通用 Bug 清单(与 Windows 无关,但每写一个新插件都会撞上)见文末「附录:通用 Bug 速查」。
三、Windows 平台专属踩坑(重点)
如果你在 Windows 上开发 Flutter OHOS 插件,下面三件事是必看的,照着做能省掉至少 2 天的弯路。
3.1 自带的example工程跑不起来,要自己建一个
现象:
flutter create生成的example/目录里,ohos/子工程跑不起来。- 在 DevEco Studio 里打开
example/ohos,hvigor 同步失败、签名缺失、依赖拉不到。 - 即便能 build,运行时也找不到插件注册(
Plugin OhosImmersiveLight not found)。
原因:
- 插件模板里
example/ohos只搭了个空壳,没有完整的build-profile.json5/hvigorfile.ts/hvigor/hvigor-config.json5,也没有正确的AppScope/app.json5。 - 它的
dependencies指向本插件的path:引用没问题,但entry模块的oh-package.json5没把插件的 har 显式声明,hvigor 不会自动把插件原生代码加进构建。
解决:自己建一个工程(参考仓库里的ohos_immersive_light/exam_app/),步骤如下:
# 1. 在插件同级目录新建一个空 Flutter 工程flutter create exam_appcdexam_app# 2. pubspec.yaml 用 path 引用本地插件dependencies:ohos_immersive_light:path:../# 3. 拉依赖(注意:不要用 git 引用,见 3.2)flutter pub get# 4. 生成 ohos 工程flutter create--platforms=ohos.# 5. 用 DevEco Studio 打开 exam_app/ohos,配置签名后 Run要点:
exam_app/ohos/hvigorconfig.ts调injectNativeModules即可(见 3.3)。exam_app/ohos/entry/oh-package.json5不需要手写插件依赖,injectNativeModules会自动注入。- 把
exam_app/当作「调试宿主」,发布前所有调试都在这里做;插件的example/可以删掉,也可以留作最小 demo,但不能拿来当联调工程。
结论:Windows 上别试图「一键跑通插件 example」,老老实实自己建一个
exam_app。
3.2 不要用git:引入依赖
现象:
pubspec.yaml里写:dependencies:ohos_immersive_light:git:url:https://gitcode.com/zjx_jason/ohos_immersive_lightflutter pub get报:路径含中文 / 含空格 /Unable to resolve、git checkout失败、ohos/目录被识别为 Git 子模块 / 软链接异常。
原因(Windows 特有):
- Windows 默认
core.longpaths没开,路径长度超过 260 字符就 GG;Flutter OHOS 工程路径通常都很深。 git:引用会让 pub 把仓库 clone 到%LOCALAPPDATA%\Pub\Cache\git\...,再用软链引回工程;在 Windows 上软链 / Junction 经常被 DevEco Studio / hvigor 拒识。- 仓库里如果带
ohos/子目录且.gitignore不完善,pub 还会把ohos/当成本地覆盖目录来链接,进一步炸。
解决:
- 开发阶段:统一用
path:引用本地插件。dependencies:ohos_immersive_light:path:../ - 发布给用户:在 README 里同时给
path:/pub:/git:三种安装方式,但你自己 Windows 联调时只用path:。 - 如果必须用
git:,请在仓库根加好.gitignore(至少忽略ohos/build/、ohos/.hvigor/、ohos/.idea/、example/、build/),并确保core.longpaths = true。
结论:Windows 开发期,pubspec 永远用
path:,别用git:。
3.3hvigorconfig.ts的srcPath必须是相对路径
现象:
- 自己用
flutter create --platforms=ohos .生成的工程,build-profile.json5里modules[0].srcPath写成了类似E:\\flutter_pub\\exam_app\\ohos\\entry的绝对路径。 - 把整个
exam_app/拷给同事、或者 CI 上拉下来换盘符后,hvigor 直接报「module not found」「path not exist」。 flutter run时偶发hvigor assembleHar failed。
原因:
- 早期某些 Flutter OHOS 模板在 Windows 上生成
build-profile.json5时会写成绝对路径。 - DevEco Studio 在某些版本上导入工程后,会主动把
srcPath改写成绝对路径(IDE 的小聪明)。
解决:
- 打开
exam_app/ohos/build-profile.json5,手动把modules[*].srcPath改成相对路径:{ "modules": [ { "name": "entry", "srcPath": "./entry", // ← 必须是相对路径 "targets": [ { "name": "default", "applyToProducts": ["default"] } ] } ] } - 同样地,宿主的
ohos/hvigor/hvigor-config.json5不要有dependencies: { "xxx": "E:\\...\\plugin\\ohos" }这种绝对依赖;通过injectNativeModules(__dirname, path.dirname(__dirname))让 hvigor 自动发现插件 har 即可。
// exam_app/ohos/hvigorconfig.tsimportpathfrom'path';import{injectNativeModules}from'flutter-hvigor-plugin';injectNativeModules(__dirname,path.dirname(__dirname));结论:每次新建 / 拷贝 OHOS 宿主工程后,先检查
build-profile.json5的srcPath,不是相对路径就改掉。
四、ohos_immersive_light实战要点(HDS TabBar)
4.1 在@Builder里使用原生组件
@BuilderfunctionhdsTabsBuilder(params:Params){ImmersiveLightInternalComponent({platformView:params.platformViewasImmersiveLightPlatformView,controller:(params.platformViewasImmersiveLightPlatformView).controller,});}这里
(params.platformView as ImmersiveLightPlatformView)内联两次是故意的——@Builder里不能let中间变量。
4.2 设备类型判断(悬浮样式门控)
HdsTabs的悬浮样式仅在phone/tablet/2in1上有效,要在原生侧用deviceInfo.deviceType做门控,TV / Watch / Car 一律回退到普通 TabBar。
functionisFloatingSupportedByDevice():boolean{constt:string=deviceInfo.deviceType;returnt==='phone'||t==='tablet'||t==='2in1';}Dart 侧也可以通过 MethodChannel 问原生「当前设备是否支持悬浮」,避免把门控逻辑写死:
finalsupported=awaitOhosImmersiveLight().isFloatingSupported();if(!supported){// 提示用户当前设备不支持悬浮样式}4.3 颜色 / 渐变字段
- Dart 端用
int传颜色(如0xFF0A59F7),原生侧按number接收。 floatingStyle/barWidth/gradientMask/miniBar这种结构化字段,原生侧用HashMap<string, Object>接收,配合parseFloatingStyle(fsMap)一类工具函数容错解析:
functionparseFloatingStyle(fsMap:HashMap<string,Object>):HdsFloatingStyleConfig{constcfg:HdsFloatingStyleConfig={};constrawBarWidth=fsMap.get('barWidth')asHashMap<string,Object>|undefined;if(rawBarWidth){cfg.barWidth={smallWidth:rawBarWidth.get('smallWidth')asnumber|undefined,mediumWidth:rawBarWidth.get('mediumWidth')asnumber|undefined,largeWidth:rawBarWidth.get('largeWidth')asnumber|undefined,};}// ... 其余字段同理returncfg;}关键原则:所有 HashMap 取值都要as <期望类型> | undefined,再判空 + 赋值;永远不要try { ... } catch { }一次性吞掉,否则类型被推断成any直接被 ArkTS 拒。
4.4 Tab 切换事件
controller.onChange((index:number)=>{constchannel=viewChannelMap.get(this.viewId);channel?.invokeMethod('onTabChange',index);});Dart 端在onTabChange回调里setState(() => _currentTabIndex = index)即可联动 UI。
ImmersiveLightView(options:ImmersiveLightOptions(tabItems:[...]),onTabChange:(index)=>setState(()=>_currentTabIndex=index),)4.5 资源图标:路径 vs PixelMap
HdsTabs的 Tab 图标支持两种形式:
- 资源路径(
iconUrl: 'assets/icons/home.png'):Dart 端把资源打进 bundle,原生侧用$r('app.media.xxx')或$rawfile(...)加载。 - PixelMap:通过 MethodChannel 把
image.PixelMap传过去,适合动态图标(未读消息数 badge 等)。
本插件默认走iconUrl资源路径,Dart 侧把assets/icons/写到pubspec.yaml的flutter.assets即可。
五、发布与维护
5.1 pubspec 必填项
name:ohos_immersive_lightdescription:"OpenHarmony HDS TabBar 插件,支持悬浮样式和沉浸光感效果"version:0.1.1homepage:https://gitcode.com/zjx_jason/ohos_immersive_lightenvironment:sdk:^3.9.2flutter:">=3.3.0"# ← 必加,否则 pub 校验会报 SDK 1.9.x 旧默认dependencies:flutter:sdk:flutterplugin_platform_interface:^2.0.2flutter:plugin:platforms:ohos:pluginClass:OhosImmersiveLightPlugin5.2 发布命令
# 1. 提交所有修改(pub 校验要求干净状态)gitadd.gitcommit-m"chore: ready to publish 0.1.0"# 2. 发布PUB_HOSTED_URL=https://pub.dev flutter pub publish# 输入 y 确认Windows 上跑
flutter pub publish时,如果工程路径深、有中文、含空格,可能卡在生成 tar 包阶段。把工程挪到短路径、无中文、无空格的盘符根目录附近通常能解决。
5.3 README 与 CHANGELOG
- README 给「兼容性表格」(Flutter / Dart / SDK / IDE / 设备类型)。
- 安装方式同时给
path:/git:/pub:三种,但自己 Windows 联调时只演示path:。 - 单独维护
docs/DEVICE_COMPATIBILITY.md,标明哪些设备型号实测通过、哪些已知问题。 - CHANGELOG 按 Keep a Changelog 写。
六、调试技巧
- 看原生日志:
hilog过滤tag == 'OhosImmersiveLightPlugin',Dart 侧print配合flutter run控制台。 - MethodChannel 入参解码失败:在原生
MethodCallHandler.handleMethodCall入口先hilog整条call.args,对 HashMap 字段,别用属性访问,只用get(key)。 - PlatformView 不显示:99% 是
@Builder内写了非 UI 语法,编译期会报;或viewType拼写不一致。 - 签名报错:
build-profile.json5里的signingConfigs.material.certpath等要指向本机.ohos/config/下的文件,换机器后不要提交该路径。 - hvigor 缓存:改
build-profile.json5/oh-package.json5后,跑一次hvigor clean再 build。 - HdsTabs 不渲染悬浮样式:先
hilogdeviceInfo.deviceType,确认设备类型在白名单(phone/tablet/2in1)内;不在就回退。
七、小结
Windows 上写 Flutter OHOS 插件,记住三件最重要的事:
- 自带的
example工程跑不通,自己新建一个exam_app/当宿主。 - pubspec 用
path:引用本地插件,不要用git:。 build-profile.json5的srcPath必须是相对路径。
把上面这套流程跑通,剩下的就是通用 Flutter OHOS 插件开发——PlatformView+MethodChannel+AbilityAware+@Builder+HashMap取值。把这套模板记熟后,换个原生组件就能复刻出第二个、第三个插件。
附录:通用 Bug 速查(跨平台)
| # | 现象 | 原因 | 解决 |
|---|---|---|---|
| 1 | Module '"@ohos/flutter_ohos"' has no exported member 'Params' | 主包未 export Params | 从@ohos/flutter_ohos/src/main/ets/plugin/platform/PlatformView子路径导入 |
| 2 | Only UI component syntax can be written here | @Builder 内写了let/const | 删掉中间变量,全部内联 |
| 3 | Property 'getPlatformViewsController' does not exist | OHOS 绑定 API 不同 | 改用binding.getPlatformViewRegistry().registerViewFactory(...) |
| 4 | Expected 1 arguments, but got 0(Factory super) | PlatformViewFactory 需要 MessageCodec | super(StandardMessageCodec.INSTANCE) |
| 5 | implements PlatformView报缺方法 | PlatformView 是抽象类 | 改extends PlatformView,只实现 getView / dispose |
| 6 | titleColors 等 private 字段报「Property is private」 | @Component 私有字段 | 去掉private修饰,或改用 Builder 内联传值 |
| 7 | HashMap 字段取到undefined | 用args['key']属性访问 | args as HashMap<string, Object>; argMap.get('key') |
| 8 | Use explicit types instead of any, unknown | ArkTS 严格模式禁 any | 写强类型as <type> | undefined,try/catch 容错回退 |
| 9 | pub 报 SDK 1.9.x 旧默认 | 没写environment.flutter | 加flutter: ">=3.3.0" |
| 10 | pub 报「1 checked-in file is modified」 | 未 commit | 先git add && git commit再 publish |
文档版本:与ohos_immersive_light0.1.x 配套;如 API 变更以仓库与 pub.dev 为准。