Flutter 和 ArkTS 在一个鸿蒙项目里应该怎么分工
很多 Flutter 鸿蒙项目后面会越来越难维护,不一定是因为能力接入太难,而是因为 Flutter 和 ArkTS 的边界一开始就没划清。页面、路由、权限、系统入口、平台调用如果混在一起,后面无论是加能力还是写教程都会越来越乱。本文结合食界探味的实际结构,讨论在一个鸿蒙 Flutter 项目里,哪些东西更适合放在 Flutter,哪些更适合放在 ArkTS,以及这样划分的原因。
适合谁看
-
正在做 Flutter 鸿蒙项目,但不确定原生层应该承担多少的人
-
已经开始接系统能力,担心后面边界越来越乱的人
-
想把 Flutter 和鸿蒙原生层分工讲清楚的人
问题背景
在 Flutter 鸿蒙项目里,一个很常见的问题是:
Flutter 和 ArkTS 都能做一点,那到底应该让谁负责什么?
如果这个问题一开始没想清楚,后面通常会出现几种情况:
-
Flutter 里开始塞很多系统能力判断
-
ArkTS 里又慢慢长出一层业务逻辑
-
页面导航一部分在 Flutter,一部分在原生
-
权限、状态、入口逻辑互相穿插
最后项目虽然能跑,但维护和讲解都会越来越困难。
先说结论:先按“业务层”和“系统层”分,再按“谁更稳定”决定归属
如果只说一个最简单的判断原则,我会这样归纳:
业务层优先放 Flutter,系统层优先放 ArkTS;边界不清的时候,再看哪一层变化更频繁、哪一层更依赖 HarmonyOS 本身。
食界探味当前就是比较典型的这种分法。
先建立一个最小分层图
如果把这个项目压缩成一个最小分层图,大致可以先这样理解:
Flutter 页面与业务层
↓
Flutter 平台边界层(core/platform)
↓
ArkTS 原生实现层(plugins)
↓
HarmonyOS 系统能力 / 权限 / 生命周期
而在这条主链路之外,还有一条“系统把用户送进来”的入口链路:
HarmonyOS 系统入口
↓
entryability / formability
↓
ArkTS 插件或入口适配
↓
Flutter 路由与页面
这两条链路一旦分清,很多“到底该放哪”的问题就会变得容易很多。
如果再具体一点,这个项目里可以先对上下面几组目录:
-
Flutter 页面与业务层:
app/lib/features/、app/lib/data/ -
Flutter 平台边界层:
app/lib/core/platform/ -
ArkTS 原生实现层:
app/ohos/entry/src/main/ets/plugins/ -
HarmonyOS 系统入口层:
app/ohos/entry/src/main/ets/entryability/ -
HarmonyOS 卡片触达层:
app/ohos/entry/src/main/ets/formability/
先把这五层对上,再去看某个能力该放哪里,判断就会稳很多。
1. Flutter 更适合承担什么
在食界探味里,Flutter 侧主要承担的是:
-
页面 UI
-
路由组织
-
状态管理
-
数据模型
-
AI 会话与业务编排
-
网络请求
这些内容主要集中在:
-
app/lib/features/ -
app/lib/core/ -
app/lib/data/ -
app/lib/app.dart
为什么这些东西更适合放 Flutter?
因为它们通常具备几个特点:
-
和页面交互强相关
-
变化频率更高
-
需要跨平台复用
-
更接近用户真正感知到的产品层
这类内容如果过早下沉到 ArkTS,后面会让业务调整成本上升很多。
Flutter 侧适合做的,通常有一个共同点
这类事情通常都满足至少两条:
-
页面变化比平台变化更频繁
-
Android / iOS / HarmonyOS 都可能复用
-
更接近产品交互,而不是系统能力
-
不依赖 HarmonyOS 独占 API
如果一个逻辑同时满足上面几条,它大概率就更适合留在 Flutter。
在食界探味里,哪些内容明显应该留在 Flutter
可以直接举几个更具体的例子:
-
搜索结果页怎么排版、怎么筛选,应该留在 Flutter
-
AI 助手页面怎么组织消息列表、输入框和播报按钮,应该留在 Flutter
-
菜品详情页怎么根据数据状态展示占位、加载和错误,应该留在 Flutter
-
原生能力返回后,页面是弹提示、改按钮状态还是刷新列表,也应该留在 Flutter
因为这些决定的都是“产品怎么呈现”,而不是“系统怎么工作”。
2. ArkTS 更适合承担什么
在食界探味里,ArkTS 侧主要承担的是:
-
HarmonyOS 系统能力调用
-
权限申请相关逻辑
-
系统入口处理
-
Form 卡片相关能力
-
EntryAbility / ExtensionAbility 生命周期承接
这部分内容主要集中在:
-
app/ohos/entry/src/main/ets/plugins/ -
app/ohos/entry/src/main/ets/entryability/ -
app/ohos/entry/src/main/ets/formability/
为什么这些东西更适合放 ArkTS?
因为它们通常具备几个特点:
-
明确依赖 HarmonyOS API
-
明确依赖权限与 Ability 生命周期
-
很难脱离平台层单独存在
-
更接近“系统如何工作”,而不是“页面如何呈现”
ArkTS 侧适合做的,通常也有一组共性
这类事情往往都满足至少两条:
-
直接依赖 HarmonyOS Kit
-
直接依赖权限申请或系统上下文
-
直接依赖 Ability / Form 生命周期
-
不做这些就无法完成系统能力调用
如果一个能力天然具备这些特征,把它继续硬留在 Flutter 往往只会把复杂度藏起来。
在食界探味里,哪些内容明显应该放到 ArkTS
同样也可以举得更落地一些:
-
麦克风权限申请和语音识别引擎创建,应该放在
SpeechRecognitionPlugin.ets -
TTS 引擎初始化、监听播报状态、停止播报,应该放在
TextToSpeechPlugin.ets -
小艺搜索、意图执行、待处理跳转缓存,应该放在
InsightIntentExecutorImpl.ets和相关插件里 -
防窥状态订阅、系统开关检测、蒙层拉起,应该放在
AntiPeepProtectionPlugin.ets
这些逻辑的共同点是:离开 HarmonyOS 上下文之后,它们本身就不成立。
真正难的不是“谁能做”,而是“谁更适合长期负责”
很多边界问题之所以会让人犹豫,是因为 Flutter 和 ArkTS 看起来都“能做一点”。
例如:
-
跳转,Flutter 能做,原生也能做
-
权限提示,页面上能提示,原生也能申请
-
状态监听,Flutter 能监听,原生也能监听
但项目分层时,判断标准不应该只是“谁能做”,而应该是:
-
谁更接近这个能力的本质
-
谁更适合长期维护
-
谁的改动对其他层影响更小
这也是为什么分工这件事,本质上是维护性问题,而不是代码量问题。
3. 食界探味里,边界是怎么落下来的
如果把这个项目的分工压缩成一句话,大概是:
Flutter 负责表达业务需要什么,ArkTS 负责把 HarmonyOS 能力真正调起来。
例子一:语音识别
Flutter 侧:
-
app/lib/core/platform/speech_recognition_channel.dart
这里负责的是:
-
定义调用入口
-
暴露
startListening/stopListening -
把结果交还给页面层
ArkTS 侧:
-
app/ohos/entry/src/main/ets/plugins/SpeechRecognitionPlugin.ets
这里负责的是:
-
申请麦克风权限
-
创建语音识别引擎
-
监听回调
-
把识别结果回传到 Flutter
例子二:文本转语音
Flutter 侧:
-
app/lib/core/platform/text_to_speech_channel.dart
ArkTS 侧:
-
app/ohos/entry/src/main/ets/plugins/TextToSpeechPlugin.ets
分工逻辑和语音识别类似:
-
Flutter 层表达“我需要播报”
-
ArkTS 层负责“具体怎么播报”
例子三:意图跳转
Flutter 侧:
-
app/lib/core/platform/intent_navigation_channel.dart
这里主要负责:
-
接收跳转意图数据
-
把
pageId映射到具体路由 -
与 Flutter 路由体系衔接
ArkTS 侧:
-
app/ohos/entry/src/main/ets/plugins/IntentNavigationPlugin.ets -
app/ohos/entry/src/main/ets/entryability/InsightIntentExecutorImpl.ets
这里主要负责:
-
接住系统搜索或意图入口
-
暂存 pending navigation
-
把系统入口转成 Flutter 能消费的数据
这一组例子很能说明边界:
-
Flutter 更接近“页面最终去哪”
-
ArkTS 更接近“系统是怎么把用户送进来的”
例子四:防窥保护
这一组也很适合拿来看边界:
Flutter 侧:
-
app/lib/core/platform/anti_peep_protection_channel.dart
这里负责的是:
-
暴露
activateCollectionProtection/deactivateCollectionProtection -
接收原生事件回传
-
把系统状态映射成 Flutter 可观察状态
ArkTS 侧:
-
app/ohos/entry/src/main/ets/plugins/AntiPeepProtectionPlugin.ets
这里负责的是:
-
接
DeviceSecurityKit -
申请和检查系统状态
-
订阅防窥状态变化
-
必要时拉起系统蒙层
这一组边界的意义很明显:
-
Flutter 不直接理解
dlpAntiPeep的底层接口 -
ArkTS 也不直接决定页面 UI 怎么改
两边各自只负责自己那一层该做的事情。
看一个能力该放哪,最实用的其实是“决策顺序”
如果你每次都临时判断“这个东西放 Flutter 还是 ArkTS”,项目会很快不统一。
更稳的方式,是每次都按同一套顺序问自己:
第一步:这个能力是不是明显依赖 HarmonyOS 独占 API
如果是,先偏向 ArkTS。
第二步:它是不是和权限、Ability、Form 生命周期直接相关
如果是,继续偏向 ArkTS。
第三步:它是不是跨平台复用的页面或业务逻辑
如果是,优先留在 Flutter。
第四步:它是“能力本体”,还是“能力结果的页面消费”
-
能力本体,偏原生
-
页面消费,偏 Flutter
只要这套决策顺序稳定下来,团队就不容易每次都做出不一样的分工。
再补一层:分工前先问的 4 个问题
上面那套决策顺序适合快速判断。
如果你正准备新接一个能力,我更建议再多问四个问题:
1. 这个能力离开 HarmonyOS 以后还剩下什么
如果离开 HarmonyOS 以后,剩下的只是一句“我要开始识别”“我要开始播报”,那说明真正复杂的部分不在 Flutter,而在原生层。
2. 这个能力未来更可能改“页面表现”,还是改“系统接法”
如果以后大概率会经常改交互、状态、按钮、引导文案,那部分优先留 Flutter。
如果以后大概率会改权限、订阅、系统参数、原生回调,那部分优先留 ArkTS。
3. 这个能力失败时,哪一层更适合解释错误
例如:
-
permission denied -
engine create failed -
ability context unavailable
这类错误更适合先在 ArkTS 层被归一化,再交给 Flutter 做页面表达。
如果让页面层直接理解原生错误细节,后面会越来越耦合。
4. 这个能力有没有系统主动推送回来的状态
像防窥保护这种能力,不只是“调用一次返回一次”,而是系统可能不断推送状态变化。
这类能力通常更适合:
-
ArkTS 负责订阅和清理
-
Flutter 边界层负责转成可观察状态
-
页面层只负责消费状态
这一问很重要,因为它能帮你分清“命令型能力”和“事件型能力”。
什么叫“边界层”,为什么它不能省
很多人理解分工时,会只想到两层:
-
Flutter
-
ArkTS
但真实项目里,最容易被忽略、也最关键的一层,其实是:
Flutter 平台边界层
在食界探味里,它主要就是:
-
app/lib/core/platform/
这一层的作用不是重复写原生代码,而是把两边隔开:
-
对 Flutter 页面来说,它隐藏原生细节
-
对 ArkTS 原生层来说,它收敛调用协议
如果没有这一层,最容易发生的事就是:
-
页面直接耦合原生回调细节
-
某个能力的参数结构散落在各个页面里
-
后面想统一错误处理或返回风格时非常困难
所以从工程角度看,边界层并不是“可选优化”,而是分工真正能成立的前提。
边界层具体负责什么
很多人第一次看到 app/lib/core/platform/,会以为这里就是简单包一层 MethodChannel。
但它真正应该承担的事情,通常比“转发一下调用”多一点:
-
统一 channel 名称和方法名
-
统一参数结构和返回结构
-
把原生事件改造成 Flutter 好消费的状态
-
屏蔽平台不存在时的异常,比如
MissingPluginException -
让页面层看不到 ArkTS 细节
以 anti_peep_protection_channel.dart 为例,它做的不只是转调方法,还承担了两件很关键的事:
-
用
setMethodCallHandler接住原生事件 -
用
ValueNotifier<AntiPeepVisibilityState>把原生状态变成页面能直接监听的状态
这就是边界层的价值。
它不是业务层,但它决定了业务层能不能保持干净。
一条完整链路该怎么走
只讲“谁放哪”还不够,真正有帮助的是把一条链路走完整。
这里可以拿防窥保护举例,因为它同时包含了:
-
Flutter 主动发起调用
-
ArkTS 调系统能力
-
系统再异步回推状态
-
Flutter 页面最终消费状态
第 1 步:页面表达业务意图
页面层不会直接碰 DeviceSecurityKit,它只表达一件事:
-
某个页面进入敏感展示状态,需要开启防窥保护
-
离开这个页面时,需要关闭保护
这类表达应该留在 Flutter 页面或页面控制器里。
第 2 步:边界层发出平台调用
app/lib/core/platform/anti_peep_protection_channel.dart 负责暴露:
-
activateCollectionProtection() -
deactivateCollectionProtection()
页面调用的是这两个语义化方法,而不是某个原生 Kit 的底层接口名。
第 3 步:ArkTS 插件接住调用并进入系统上下文
app/ohos/entry/src/main/ets/plugins/AntiPeepProtectionPlugin.ets 接到调用后,会开始处理真正的平台动作:
-
检查是否已经激活
-
检查系统防窥开关状态
-
必要时请求开启选项
-
订阅
dlpAntiPeep状态变化 -
在需要时设置系统蒙层
这些都明显属于 HarmonyOS 原生层职责。
第 4 步:系统状态变化回推到 Flutter
插件里不是只 result.success(null) 就结束了。
它还会通过 channel.invokeMethod('onAntiPeepEvent', args) 主动把事件推回 Flutter。
像这些事件:
-
HIDE -
PASS -
DEACTIVATE -
MASK_SHOWN
都说明这不是一次性的“命令调用”,而是一条持续的事件链路。
第 5 步:边界层把原生事件翻译成页面状态
Flutter 侧收到 onAntiPeepEvent 后,没有把原生事件字符串直接丢给页面,而是先做了一层转换:
-
HIDE对应AntiPeepVisibilityState.hidden -
PASS/DEACTIVATE对应AntiPeepVisibilityState.visible
这样页面层消费的就是稳定的 Flutter 状态,而不是一组平台事件字符串。
第 6 步:页面层只根据状态改变展示
到页面这一层,只需要回答:
-
当前内容该不该隐藏
-
是否需要停留在安全占位态
-
是否要提示用户当前不可见
页面不用知道:
-
dlpAntiPeep是什么 -
订阅是怎么注册的
-
蒙层是怎么拉起的
这就说明分工是成立的。
每一层都只承担自己最应该承担的那部分复杂度。
命令型能力和事件型能力,分工要稍微不同
并不是所有能力都像语音识别或 TTS 那样“调用一下,得到结果”。
在食界探味里,至少可以粗分成两类:
命令型能力
例如:
-
开始播报
-
停止播报
-
发起一次导航
这类能力的特点是:
-
页面发出动作
-
原生侧执行
-
返回成功或失败
这时边界层更像一个“语义化调用入口”。
事件型能力
例如:
-
防窥状态变化
-
语音识别过程中的中间回调和最终结果
-
系统入口带来的待处理跳转
这类能力的特点是:
-
系统可能持续推送状态
-
原生层需要负责订阅和释放
-
Flutter 边界层需要负责把事件转成页面能消费的状态
如果把事件型能力也当成最简单的同步调用去设计,后面大概率会返工。
4. 哪些东西最好不要放错层
这是很多项目后面变乱的根源。
不建议大量放到 ArkTS 的东西
-
复杂业务判断
-
页面级状态管理
-
大量与平台无关的数据处理
-
本来就应该由 Flutter 页面控制的导航流程
-
按产品需求频繁变化的展示逻辑
不建议硬留在 Flutter 的东西
-
依赖 HarmonyOS 权限的能力调用
-
Speech Kit / TTS / 防窥这类系统 API
-
Form 卡片生命周期
-
EntryAbility / Intent 执行入口
-
依赖 Ability 上下文和系统窗口对象的能力
还有一类东西,最容易被两边都误收
这一类通常是:
-
页面跳转映射
-
原生事件到 Flutter 状态的转换
-
平台错误到业务错误的转换
它们不完全属于 Flutter 页面层,也不应该塞进 ArkTS 业务层。
更合适的落点通常就是前面说的边界层。
换句话说:
-
业务层不要被原生层吞掉
-
系统层也不要被 Flutter 假装包住
-
中间协议层也不要凭空消失
最常见的错误分工长什么样
如果想更直观一点,可以直接看几个反例。
反例 1:ArkTS 决定最终跳哪个业务页
短期看很省事,但后面只要 Flutter 路由结构改了,原生层也要跟着一起改。
更稳的做法是:
-
ArkTS 只负责把系统入口整理成
pageId或待处理导航数据 -
Flutter 边界层或路由层负责把它映射成具体页面
反例 2:Flutter 页面自己处理所有权限分支
短期看逻辑都在页面里,但后面每个页面都可能写出一套不同的权限处理方式。
更稳的做法是:
-
权限申请和底层失败原因由 ArkTS 收口
-
Flutter 只决定“怎么提示用户”和“失败后页面怎么退让”
反例 3:每个能力都自创一套参数和错误返回格式
短期能跑,长期平台层会越来越碎,教程也会越来越难写。
更稳的做法是:
-
在
core/platform/里统一命名风格 -
统一参数结构
-
统一错误兜底方式
反例 4:系统入口直接穿透到页面层
这样页面会越来越知道系统协议细节,最终项目会越来越难重构。
更稳的做法是:
-
entryability/负责接系统入口 -
插件或入口适配层负责整理数据
-
Flutter 路由层只消费已经约定好的导航信息
可以直接套用的一套分工表
如果你正在搭自己的项目,下面这张分工表基本可以直接拿去用:
|
层级 |
建议放什么 |
不建议放什么 |
|---|---|---|
|
Flutter 页面层 |
UI、交互、状态消费、路由跳转 |
HarmonyOS Kit 细节、权限底层分支 |
|
Flutter 边界层 |
Channel、事件翻译、平台错误收口、语义化方法 |
大量页面状态、复杂业务规则 |
|
ArkTS 插件层 |
Kit 调用、权限、订阅、上下文、窗口、引擎管理 |
页面跳转细节、业务编排 |
|
entryability / formability |
系统入口、卡片触达、Ability 生命周期 |
页面展示逻辑、产品判断 |
如果你发现某段代码同时想做两行甚至三行的事,通常就说明这段代码已经跨层了。
5. 为什么这件事对技术教程也特别重要
如果边界不清,不只是代码会乱,文章也会很难写。
因为你会发现自己讲不清:
-
这个能力到底在哪一层实现
-
这一段逻辑为什么放在这里
-
Flutter 代码和 ArkTS 代码分别解决什么问题
而食界探味现在这套结构之所以适合拆成教程,就是因为它的边界相对容易讲:
-
core/platform/是 Flutter 的能力边界 -
plugins/是 ArkTS 的能力实现层 -
entryability/是系统入口层 -
formability/是系统触达层
这会让文章更容易拆成:
-
一篇讲分工原则
-
一篇讲某个 channel
-
一篇讲某个插件
-
一篇讲某个系统入口
常见坑
坑 1:ArkTS 写着写着变成第二套业务层
一旦原生层开始承担太多页面判断和业务拼装,后面会非常难维护。
坑 2:Flutter 假装什么都能做
有些能力本身就强依赖 HarmonyOS 系统层,硬留在 Flutter 只会让边界更脆。
坑 3:导航边界不清
系统入口带来的跳转和 Flutter 内部路由混在一起时,问题会特别多。
坑 4:写教程时只讲某一边
只讲 Flutter 或只讲 ArkTS,都容易把实际链路讲断。
坑 5:一开始没定规则,后面每个能力都单独拍板
这种项目最容易在半年后出现“每个能力风格都不一样”的情况。
可复用模板
如果你也在做 Flutter 鸿蒙项目,可以先这样划分:
放 Flutter
-
页面
-
路由
-
状态
-
数据模型
-
业务编排
放 ArkTS
-
系统能力
-
权限
-
系统入口
-
卡片
-
Ability 生命周期
放边界层
-
Channel 定义
-
平台数据协议
-
页面跳转映射
-
原生事件到 Flutter 状态的转换
-
平台异常兜底和兼容处理
这套分法不是唯一答案,但对大多数项目来说,是一个比较稳的起点。
如果你想再进一步把它落成可执行规则,也可以补一句:
-
页面层不直接调系统细节
-
原生层不直接做业务编排
-
平台边界层统一协议与事件
这样团队在实际开发时会更容易达成一致。
本篇总结
Flutter 和 ArkTS 在一个鸿蒙项目里该怎么分工,核心不在“哪边更强”,而在“哪边更适合承担哪一层职责”。
食界探味当前的结构比较清楚地说明了这件事:
-
Flutter 负责业务和页面
-
ArkTS 负责系统能力和系统入口
-
平台边界层负责把两者连起来
真正好用的分工,不只是能解释现状,还应该能指导下一个能力怎么接。
如果你后面继续接语音、TTS、Intent、卡片或者防窥能力,都可以先按本文这套判断顺序过一遍:
-
先看是不是 HarmonyOS 独占能力
-
再看是不是系统上下文问题
-
再看是不是页面消费问题
-
最后把协议和状态翻译收口到边界层
只要这个边界一开始就划清,后面无论是继续开发还是继续拆成教程,都会顺很多。
更多推荐




所有评论(0)