今年这个开源鸿蒙跨平台训练营走到第14天,刚好卡在第二阶段收尾。按训练营的节奏,第一阶段解决的是“跑起来”,环境装好、工程建好、能在模拟器上看到Hello World;第二阶段就不一样了,要求每个人能独立写出能交差的业务模块:有页面跳转、有状态管理、能调原生能力、能打包运行。趁热打铁把这两个星期学到的东西完整复盘一遍,既是给自己的知识做沉淀,也给后面想走同一条路的开发者一份可参考的路线图。这篇文章覆盖的核心关键词就是 Flutter for OpenHarmony,后面简称FFO,我会把第二阶段掌握的知识点按“工程结构、UI组件、路由状态、平台通道、网络存储、打包调试、避坑经验”的顺序展开。
1. 第二阶段到底学了什么:先给这份知识地图
复盘的前提是知道边界在哪。这一段经历不是从零学Flutter,而是把已有的Flutter开发经验迁移到OpenHarmony上,同时补齐平台差异带来的额外知识。
1.1 第二阶段的定位:从能跑到能独立交付
第一阶段的目标很纯粹——把开发环境搭起来,跑通一个最小应用。很多人以为这一步最费劲,实际上真正让人卡住的是第二阶段。因为第二阶段默认你掌握了Flutter基础语法和组件用法,然后开始接触真实项目才会遇到的东西:多页面如何组织、数据从哪里来、状态在哪里维护、原生系统的能力要怎么打通。
我给自己定的第二阶段验收标准是三条:
- 能独立创建一个FFO工程,并清楚理解Dart层和ohos原生层的关系
- 能完成一个包含“列表加载、详情跳转、本地缓存、原生能力调用”的完整业务闭环
- 能把最终产物打成hap包,在真机上正常安装运行
这三条标准看起来不复杂,但每一环展开都有大量细节。后面所有内容,本质上都是围绕这三条标准在补充弹药。
1.2 复盘方法:先用一条业务主线把零散知识点串起来
知识点如果只是逐个记忆,过两周基本就忘了。训练营第二阶段给自己定的复盘方法很简单:找一条真实的业务主线,把学过的知识像穿珠子一样全部串上去。我选的这条线是“登录→列表→详情→缓存→调用系统能力”。
这条线几乎能覆盖所有核心知识点。登录要处理网络请求和表单;列表要处理状态管理和异步加载;详情页要处理路由传参;缓存要处理本地存储方案;最后调用系统能力要走平台通道。任何知识点只要回答不出“这条主线里哪个环节会用到它”,就说明还没掌握到位。
复盘的时候建议画一张脑图,中心写“Flutter for OpenHarmony”,分四层:环境层、框架层、桥接层、发布层。环境层是SDK和工具链,框架层是组件与状态管理,桥接层是平台通道,发布层是打包签名。第二阶段所有内容都落在框架层和桥接层,我自己把它们叫“承上启下的最关键两圈”。
2. 工程结构与环境:FFO项目是两个世界的合成体
要理解FFO项目,就必须先接受一个事实:一个工程文件夹里同时住着两个世界。一个是Flutter的Dart世界,负责业务逻辑和UI;另一个是OpenHarmony的原生世界,负责系统能力和应用壳。这两个世界平时各写各的,只在特定入口发生交互。
2.1 环境问题的本质:版本匹配
第二阶段开始前,有不少同学还停留在“flutter doctor通过就算环境OK”的认知里。复盘之后才发现,FFO环境的坑基本全在“版本匹配”上:Flutter SDK的版本要匹配OpenHarmony SDK的版本,原生部分使用的API版本又要匹配设备的系统版本,三方库的适配版本也得对应。
建议把以下三个版本信息单独记在一个文档里,每次排查问题先对号入座:
- Flutter SDK版本:决定Dart语言特性和框架API
- OpenHarmony SDK版本:决定原生侧可用的API能力和编译目标
- 设备系统版本:决定运行时的行为差异
环境排查的基本操作还是flutter doctor,不过要重点看有没有识别出ohos环境。如果没识别出来,大概率是Flutter SDK用的不是FFO适配的分支,或者路径配置没生效。这类问题一旦定位到“版本”,解决起来通常就是换一个匹配的分支重新拉取,然后耐心等地依赖下载完。
2.2 一个工程里的两个世界:目录结构解读
用一个标准FFO工程来看,顶层目录大概是这样的:
my_app/ ├── lib/ # Dart层代码,业务逻辑都在这 ├── ohos/ # OpenHarmony原生工程 │ ├── entry/src/main/ # 应用入口、配置文件 │ ├── entry/src/ohosTest/ │ └── build-profile.json5 ├── pubspec.yaml # Dart依赖声明 ├── oh-package.json5 # 原生侧依赖声明 └── build/ # 构建产物lib目录对任何Flutter开发者都不陌生,真正需要下功夫的是ohos目录。里面有模块配置、应用图标、权限声明、还有原生侧入口。在开发过程中,大量问题出在开发者只改Dart层不改原生配置,比如要调用相机权限,只在Dart代码里写逻辑,忘了去原生配置里声明权限。
有一点特别提醒:在老版本FFO工程里,添加OpenHarmony平台支持不是默认行为。如果从一个普通Flutter工程起步,需要手动添加平台支持。常见做法是在工程根目录执行类似下面的命令:
flutter create --platforms ohos .执行完以后会生成ohos目录,之前写的lib代码基本不用动,这就是FFO工程结构上的最大优势:Dart层和平台层是解耦的。
2.3 热重载的边界:别拿原生的习惯去套
Flutter的hot reload体验确实很爽,但在FFO上要分清楚边界。改动lib目录下的Dart代码,热重载基本稳定;改动原生侧的代码,比如修改了插件调用逻辑或者原生配置,热重载大概率不生效,甚至会产生“看起来改了但跑起来还是旧逻辑”的诡异问题。
我自己养成的习惯是:只动Dart代码用热重载,动了原生代码就老老实实停掉应用,重新构建。虽然多了几秒钟等待,但能避免缓存导致的状态错乱。排查诡异问题时,也永远是“先停掉、再重新构建”,这句话在训练营里救了很多次场。
3. UI与组件:可以照搬Flutter习惯,但要认清边界
Flutter for OpenHarmony的UI体系基本沿袭了标准Flutter:Widget树、Element树、RenderObject树那套底层机制没变。所以你在Android平台上写过的Row、Column、ListView、Stack在这里依然有效。但这不代表没有任何差异,第二阶段最容易忽略的就是“组件可用”和“组件行为一致”之间的差距。
3.1 核心Widget在OpenHarmony上的表现差异
以Material组件为例,Button、TextField、Card这些基础组件在FFO上都能正常使用,因为Flutter引擎自己完成绘制,不依赖系统原生控件。但有几个细节要做额外适配:
- 文本输入:输入法的弹出时机、软键盘避让行为,在不同设备上表现不一
- 对话框:Dialog的样式是Flutter自己画的,但在返回键处理和点击遮罩关闭的行为上,需要测试真机表现
- 字体渲染:中文字体在不同设备上的默认字体可能不一致,排版时不要把固定高度写死
训练营里分享过一个经验:凡是“看着像Flutter标准行为”的功能,都要在OpenHarmony真机上再过一遍,不能因为Android上是这样就直接交差。
3.2 多端适配的实践
OpenHarmony设备形态很多,从手机到平板再到开发板,屏幕尺寸和密度差异很大。第二阶段的UI适配教了三个最常用的工具:
- MediaQuery:获取屏幕宽高、像素密度、系统字号配置
- LayoutBuilder:根据父级约束动态选择布局方案
- FractionallySizedBox、Flexible、Expanded:用比例代替绝对像素
核心心法就一句:先定比例,再定像素。我用一个简单例子来说明,一个首页顶部卡片,在手机上希望占40%宽度,在平板上希望占25%宽度,正确做法不是写两个分支布局,而是用LayoutBuilder拿到的宽度,结合MediaQuery判断设备类型,再计算出一个flex比例。这样后续适配新尺寸的设备,只需要改判断阈值。
3.3 组件拆分的原则
第二阶段开始要求大家写“能复用”的组件,而不是把所有代码堆在一个页面文件里。复盘时总结了三个拆分原则:
- 一个组件只做一件事,从组件名字就能看出职责
- 状态尽量往上提,展示型组件保持无状态
- 列表项组件单独提取,避免列表重建时大面积rebuild
这里特别提一下const的使用。在build方法里创建子组件时,如果参数不变,就加const关键字。别小看这个操作,在列表页这种高频build场景下,const能直接跳过组件重建的步骤,实测对滚动流畅度帮助很明显。
4. 路由、状态与生命周期:应用复杂化的三根支柱
第二阶段的教学内容明显从单一页面跳到了多页面应用,这意味着要对导航栈、状态共享、生命周期这三个话题有认知升级。
4.1 路由方案:从一个页面到多个页面
路由管理是第二阶段每天都要用的知识点。FFO上的路由方案跟标准Flutter一致,有两种选择:
- Navigator 1.0:直接用Navigator.push/Pop,适合中小型项目
- Navigator 2.0:用Router和PageRoute等信息管理页面栈,适合对深度链接有要求的项目
训练营第二阶段着重练的是1.0,因为简单直观,理解和排查成本低。典型场景是列表页传给详情页一个ID参数,详情页处理完再返回刷新列表:
// 跳转并等待返回结果 final result = await Navigator.push<String>( context, MaterialPageRoute(builder: (context) => DetailPage(id: item.id)), ); if (result != null) { // 根据返回结果刷新列表状态 }路由命名的方式也值得提倡,用静态常量集中管理路由地址,避免字符串散落在代码各处。在第二阶段项目里,我发现大部分路由相关的bug都源于字符串写错,集中管理后这个问题基本绝迹。
4.2 状态管理:从setState到更可靠的方式
课程到第二阶段后半程引入了状态管理,原因很简单,页面变多以后,兄弟组件之间要共享数据,只用setState会写成一场灾难。训练营给出的进阶路线是:
setState → Provider → Riverpod
不建议一上来就用很重的状态管理框架,因为FFO生态下的第三方库适配情况参差不齐。先掌握Provider,它的核心概念好理解:在组件树上层放一个Provider,下面的组件通过context去读取和修改状态。等真正遇到“依赖多个异步状态、需要组合派生状态”的场景,再升级到Riverpod也不迟。
有一点要特别注意:FFO工程里引入状态管理库,一定要确认该库的纯Dart性。如果依赖了某个平台插件,而该插件没有适配OpenHarmony,运行时会直接报缺失实现的错误。原理上,Riverpod、Provider这类库是纯Dart实现的,风险很低;如果依赖链里出现某个原生插件,就要提前检查有没有对应的_ohos适配包。
4.3 生命周期:从小处观察App状态
生命周期知识在第二阶段的意义,是处理“应用退到后台再回来”的场景。比如一个计时器应用,用户锁屏后再回来,如果不对生命周期做处理,计时器就可能错乱。
Flutter里监听App生命周期变化有标准方案:
class AppLifecycleObserver with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { // 处理前后台切换 } }需要在initState里注册观察者,在dispose里移除。实操中容易漏掉dispose,轻则造成重复回调,重则组件已销毁还在接收事件导致异常。养成一个习惯:任何addListener、addObserver,都要在同一处写对应的remove逻辑。
5. 平台通道:Flutter和OpenHarmony之间的真正桥梁
如果说第二阶段前半程是“用Flutter的习惯写页面”,后半程的学习重心就是“让Flutter代码和OpenHarmony系统能力对话”。这个对话机制在Flutter里叫平台通道,在FFO里是同样的概念,但原生侧实现细节不同。
5.1 三种平台通道,各管各的场景
- MethodChannel:请求-响应模式,最常用,适合“调用一个方法并拿回结果”
- EventChannel:事件流模式,适合“持续接收原生侧推送的数据”
- BasicMessageChannel:双向消息传递,适合更复杂的通信需求
第二阶段用得最多的是MethodChannel。比如读取设备电量、获取设备型号、调用系统分享等,都属于典型的请求-响应场景。EventChannel则在传感器数据、系统广播这类场景中更合适。
5.2 Dart侧和原生侧的配合写法
以获取设备电量为例,Dart侧代码约定通道名和一个方法名:
class BatteryService { static const MethodChannel _channel = MethodChannel('com.demo.device/battery'); static Future<int> getBatteryLevel() async { final int level = await _channel.invokeMethod('getBatteryLevel'); return level; } }原生侧需要有一个对应的通道处理器,注册同样的通道名,解析方法名,调用系统能力后返回结果。逻辑上有点像“服务端接口”,通道名就是URL,方法名就是接口路径。
原生侧实现的关键点是通道名的字符串必须完全一致。漏掉一个字母都不会直接报错,而是表现为方法调用超时或者方法找不到,这个排查成本比较高。
5.3 权限与隐私:桥接之后立刻遇到的现实问题
调用系统能力经常会触发权限管理。OpenHarmony的权限模型有自己的分类和申请流程,不能直接把习惯照搬过来。
一些关键权限需要在应用配置文件里声明,然后在代码里动态申请。动态申请要走系统提供的权限请求接口,并且要在正确的上下文环境中调用,时机不对会直接失败。训练营里遇到过“在页面初始化时立刻请求定位权限”被系统拒绝的案例,原因就是页面还没完成初始化就去请求,应该在拿到页面上下文之后再触发请求。
另一个经验是模拟器和真机的权限行为不一样。有的权限在模拟器上直接放行,在真机上却严格拒绝。所以涉及权限的功能,必须在真机上做最终验收。
6. 网络、数据持久化与业务闭环:第二阶段的重头戏
第二阶段的项目实战绕不开网络请求和数据存储。这部分内容在标准Flutter里很成熟,但要适配FFO,就要挑那些不依赖特定平台能力的方案。
6.1 网络请求:dio是更稳的选择
原生Dart的HttpClient能用,但开发效率不高,而且很多高级功能要自己封装。训练营第二阶段推荐直接使用dio库,原因是它本身是纯Dart实现,FFO上适配风险低,而且自带拦截器、超时控制、表单上传这些常用功能。
一个重要的注意点是HTTPS证书校验。默认情况下dio会使用系统证书链,但在OpenHarmony上如果遇到自签名证书或测试证书,请求会直接失败。建议开发阶段统一走测试环境,上线前再替换正式证书,不要图省事关掉证书校验。
网络请求回调里操作UI时,一定要做mounted判断:
if (!mounted) return; setState(() { _items = data; });异步请求回来时组件很可能已经销毁,不判断mounted会触发异常,这个问题在页面频繁跳转的应用里特别常见。
6.2 本地存储:可用方案的优先级排序
第二阶段的本地存储需求基本是两类:缓存用户设置项、缓存列表数据。常用的方案有这么几个:
- shared_preferences:适合存键值对、用户偏好,需要确认当前FFO版本是否有可用的实现
- 文件存储:用path_provider拿到应用目录,写JSON文件,适合存结构化数据
- 数据库:数据量大且需要查询时再引入
训练营的推荐是,第二阶段的项目优先用shared_preferences加文件存储,不要过早引入数据库。很多场景下一个JSON文件就解决了问题,引入数据库带来的复杂度远大于收益。
6.3 一个完整业务闭环的复盘案例
第二阶段项目要求做一个“登录→列表→详情→缓存”的闭环。整个流程跑下来,知识点串起来的逻辑是这样的:
用户输入账号密码,表单验证通过后,发网络请求拿到token。token保存到本地偏好存储里,后续请求通过拦截器自动携带。列表页从网络拉数据,同时把JSON缓存到本地文件,下次启动先读缓存再静默刷新。点击列表项,路由传参进入详情页,详情页展示之后可以返回修改列表状态。
这个闭环里每一次数据流转都对应一个知识点,复盘时只要能把这条链路讲清楚,第二阶段的掌握度就算合格了。
7. 打包调试与性能优化:产品化之前的热身
第二阶段结尾教了打包运行,这块内容在纯学Flutter时很容易被忽略,但在FFO上是必须掌握的硬技能,因为OpenHarmony应用最终交付的产物格式是hap包,不是apk。
7.1 调试三板斧:日志、断点、原生日志
FFO调试有三层手段:
- Dart层日志:用debugPrint输出,注意release包里会被优化掉
- Dart层断点:DevEco Studio或VS Code连接调试服务
- 原生层日志:通过HDC命令抓取系统日志,按关键字过滤
遇到“Dart代码看起来没执行”的问题,先看原生层日志,因为可能错误发生在平台通道的原生侧。区分Dart层和原生层问题,是第二阶段排查问题的核心思路。
7.2 构建HAP与签名
开发阶段构建HAP的命令是:
flutter build hap --debug这个命令会触发完整编译流程,最后在build目录下生成hap包。接下来可以用HDC命令安装到设备上:
hdc install <path-to-hap>签名这步值得多说两句。OpenHarmony应用有两种签名:调试签名和发布签名。调试签名通常在环境配置时自动生成,能保证应用在设备上安装运行;发布签名需要配置证书和密钥,一旦配置错误,轻则装机失败,重则应用运行异常。
训练营第二阶段的建议是:先搞懂调试签名,把流程走通,再去研究发布签名。不要在项目初期就纠结签名问题,那是第三阶段才需要深挖的事。
7.3 性能优化:花小力气解决大问题
第二阶段的性能优化不用上特别高深的手段,重点就三个方向:
- 列表性能:确保列表项组件提取复用,图片有缓存,避免大量无谓重建
- 帧率卡顿:减少build方法里的耗时操作,把计算、I/O都放到异步队列
- 包体积控制:避免引入用不到的大库,图片资源按分辨率压缩
有一个经验值得分享:列表滚动卡顿的排查首先看有没有在build方法里做文件读取或者JSON解析,有就全部移到异步初始化里。这个优化往往比改动任何渲染逻辑都更有效。
8. 第二阶段踩坑清单:按实测频率排序
复盘如果不把坑记下来,基本等于白复盘。我把第二阶段大家踩过的坑按出现频率排了个序,做成一个速查表:
| 坑的类型 | 表现 | 解决思路 |
|---|---|---|
| 版本不匹配 | 编译报错、API找不到 | 先对版本清单,再动代码 |
| 平台通道名不一致 | 调用超时或方法不存在 | 比对Dart层和原生层字符串 |
| 原生权限未配置 | 功能静默失败 | 检查原生配置文件权限声明 |
| 热重载后状态错乱 | 改了代码但行为没变 | 停止运行后重新构建 |
| 异步回调未判断mounted | 页面销毁后抛异常 | 网络回调统一加mounted判断 |
| 三方库缺OH适配 | 运行时提示找不到实现 | 引入前检查依赖链 |
上表前三条占了训练营里遇到问题的一半以上。原因也很清晰:FFO处于生态建设期,很多组件和插件都是跟版本强绑定的,版本错位就会引起连锁反应。
还有一个比较隐蔽的心态坑,就是遇到问题先怀疑FFO本身。实际上很多问题在标准Flutter里一样会发生,比如没做mounted判断、路由传参错误、状态管理滥用。先按标准Flutter的排查思路走一遍,再考虑平台差异,效率高得多。
9. 下一阶段学习的个人建议
按照训练营进度,第三阶段应该是更底层的引擎适配与性能攻坚,或者更复杂的原生能力深度集成。基于第二阶段的复盘,我已经能看清自己的薄弱环节:一个是平台通道的复杂类型编解码,一个是原生侧工程的综合调试能力。这两个不补扎实,后面做复杂业务会非常吃力。
给同样走在FFO路上的朋友一个建议:不要急着学很多框架,第二阶段的重点是把“一条业务闭环”吃透。能把一个项目从创建到打包完整体验一遍,比看十篇技术分享都有用。遇到版本报错也别慌,记住那个万能动作——先对比版本,再搜解决方案。
最后留个私货:我在第二阶段每完成一个小目标,都会把当时遇到的坑记在一个单独文档里,每个坑只记三行:“现象、原因、解决”。两个星期下来整理出四十多条,排查问题时翻一翻,很多问题不是不会,只是忘了曾经踩过。这个习惯算是这14天里最值钱的产出之一。