1. 为什么要在 OpenHarmony 上跑 Flutter:商城项目的选型复盘
先说结论:如果你的团队已经有成熟的 Flutter 电商业务代码,现在要拓展 OpenHarmony 生态,直接跑通结算这最后一个闭环模块,是最稳妥的切入方式。为什么这么说?因为商城类 App 的结算链路几乎涵盖了 Flutter 工程迁移到 OpenHarmony 时能遇到的所有典型问题:混合栈通信、原生插件调用、页面状态恢复、异步异常处理,甚至渲染引擎的兼容性。把结算做通了,整个 App 迁移的底子就稳了。
我在接到这个项目时,团队内部其实有过一次比较大的争论:要不要直接用 ArkUI 重写?当时评估下来,商城里光是商品详情、购物车、订单列表这些页面就有几十个,全量重写的成本足够再做半个 App。而 Flutter 侧的业务逻辑、状态管理、网络层都是现成的,OpenHarmony 官方对 Flutter 的兼容性支持也已经过了可用的门槛。与其推翻重来,不如在保持 Flutter 业务代码不变的前提下,把平台适配层做扎实,先拿结算这个"交易最后一公里"来练兵。
这里有一个背景需要先对齐:OpenHarmony 上的 Flutter 并不是直接把 Flutter 引擎塞进鸿蒙里跑,而是通过 OpenHarmony 的 Flutter SDK 适配仓(flutter_flutter、flutter_engine、flutter_plugins)完成的。华为和社区一直在同步维护这个分支,目前对 Flutter 3.7 以上的版本支持都比较稳定。你在 LTS 版本上开发的 Flutter 项目,迁移成本主要不在 Dart 代码,而在原生依赖和插件上。
结算模块恰好就是原生依赖的重灾区:支付 SDK、地址解析、风控校验、优惠券核销,这些都是要跟系统能力和第三方 SDK 打交道的。所以我把结算作为 OpenHarmony 移植的第一个完整业务闭环,不是拍脑袋,而是因为它能最大程度逼出所有兼容性问题。
另一个现实原因是 XTS 认证。OpenHarmony 设备的应用上架前通常要走 XTS 兼容性测试,这里面包含对应用稳定性、权限声明、资源占用的一系列检测。结算这种重交互、多异步任务的场景,跑一遍 XTS 能提前暴露很多问题,比如后台进程被回收后状态怎么恢复、权限拒绝后链路怎么降级。这些问题在开发环境里很难主动触发,但结算流程天然就会踩到。
2. 工程搭建与适配层准备:从 AAR 集成到 XTS 认证
2.1 Flutter 工程接入 OpenHarmony 的完整链路
如果你之前只做过 Android 和 iOS 的 Flutter 开发,第一次接触 OpenHarmony 的接入可能会有点不习惯。它不是简单地加一个平台目录,而是要借助 DevEco Studio 创建一个 HarmonyOS 的工程外壳,再把 Flutter 的产物以 Module 的方式挂进去。
具体流程我简化成四步:
- 在 Flutter 工程根目录执行
flutter build hap(或者通过 DevEco 的 hvigor 构建工具链打包出 Flutter 的产物包); - 用 DevEco Studio 创建一个空的 OpenHarmony 工程,作为宿主;
- 把 Flutter 产物作为依赖引入宿主工程,同时配置
module.json5,声明 Flutter 容器所要的权限; - 在 Ability 的页面里加载 Flutter 容器,用 Flutter 的
FlutterAbility或FlutterFragment承载 Dart UI。
这里有个关键细节:Flutter 产物在 OpenHarmony 上最终会被打包成一个 AAR 格式的模块,然后嵌进 HAP(HarmonyOS Ability Package)里。所以你在 Android 上熟悉的 AAR 概念可以完全平移过来理解,只不过这个 AAR 里的引擎变成了 OpenHarmony fork 版本。
用命令行创建宿主工程的方式是:
flutter create --platforms=ohos my_market_app如果你用的是 Flutter 3.7 之后的版本,并且安装了 OpenHarmony 的 Flutter SDK 插件,执行完这一条命令后,工程目录下会多出一个ohos文件夹。之后在 DevEco Studio 里直接打开这个文件夹,就能进行 HAP 的编译和签名了。
2.2 AAR 集成方式与 XTS 认证的关系
很多人在这一步会忽略一个东西:XTS 认证会检查应用是否按照 HarmonyOS 的规范声明了 extension abi,以及你的 Flutter 引擎 so 是否完整打包进了 HAP。如果 Flutter 的产物只是以 debug 模式编出来的,so 文件缺失或者 abi 不匹配,XTS 的静态检查阶段就直接挂掉了。
所以我在接 XTS 认证前,会专门跑一遍 release 模式的构建,确认 HAP 里的libs目录下存在arm64-v8a和x86_64两套 so。配置如下:
{ "abi" : ["arm64-v8a", "x86_64"], "target" : "hap", "mode" : "release" }然后在 DevEco 的build-profile.json5里,把signingConfigs的material路径指到你申请好的证书。证书申请本身没什么好说的,重点是你需要把 OpenHarmony 的profile文件里声明的指纹和包名,与 Flutter 侧配置保持一致,否则装到真机上会直接报Signature verification failed。
2.3 基础组件的兼容性核对清单
把 Flutter 跑起来只算第一步。到了结算页,你会立刻遇到组件和原生能力对不齐的问题。这里我列一份我在接入时逐项核对过的清单,你直接当对照表用:
- 网络请求库(dio):底层 socket 依赖 okhttp 或 cronet 的 OpenHarmony 实现,需要替换为适配版,否则会抛
SocketException; - 本地存储(shared_preferences):需要替换为 OpenHarmony 的 preferences 实现,或者直接用文件 IO;
- 支付 SDK:原生侧的支付宝/微信 SDK 是否有 OpenHarmony 版本,这个是商务层面的事,技术侧要做的是把 MethodChannel 封装好,让 Flutter 层不感知底层是哪个支付 SDK;
- 地图与定位:商城的收货地址选择通常要调地图,OpenHarmony 上建议用系统自带的 location kit,通过 MethodChannel 暴露给 Flutter;
- WebView:结算页里的用户协议、发票信息往往要用 WebView 承载,Flutter 侧的 webview_flutter 插件在 OpenHarmony 上有对应的 fork 版本,注意要用
flutter_ohos_webview这类适配仓。
注意:不要只测你常用的那条链路。结算模块涉及支付结果回跳、地址选择、优惠金额计算、发票信息填写等多个子页面,任何一个组件不兼容都会拖垮整个流程。我建表格逐项打勾的目的,就是避免上线前才发现问题。
3. 结算页面的 UI 与状态管理:组件通信是重头戏
3.1 页面结构与数据流规划
商城的结算页面(CheckoutPage)通常由这几块组成:收货地址卡片、商品清单折叠区、金额明细(商品总额、运费、优惠、实付)、支付方式选择(在线支付/货到付款)、提交订单按钮。页面本身不算复杂,复杂的是它依赖的数据源:购物车状态、用户默认地址、可用优惠券列表、库存实时状态。
在 Flutter 里我用了provider做状态管理,结算页的数据流是这样的:
- 进入结算页前,从购物车模块拿到商品列表;
- 进入页面后异步请求地址列表、优惠券列表和运费规则;
- 渲染期间如果地址或优惠券发生变化,通过回调刷新金额明细;
- 点击提交订单时,先把整个订单快照回传到服务器,拿到预支付单号,再拉起支付。
这个流程在 Android 上我写过很多遍,到了 OpenHarmony 上,Dart 层代码一行都不用改。但有几个细节必须注意,后面会展开讲。
3.2 组件通信方案选型
热词里有一个"flutter组件通信",正好戳中结算页的痛点。因为结算页不是单一页面,而是由多个子组件聚合而成。我用的是 provider + 一个轻量的事件总线来做跨组件通信。
事件总线我选了event_bus包。比如用户在地址选择页修改了默认地址,地址页直接通过 EventBus 发射一个AddressChangedEvent,结算页的商品金额区和配送信息区各自监听这个事件,分别更新配送费预估和地址展示。
为什么不全都塞进 provider?因为地址选择页和结算页并不在同一个路由栈里,强行共享状态反而会让 provider 的 model 层承担太多职责。事件总线在跨页面场景下的优势是解耦,改完地址、选完优惠券,发一个事件出去,监听方各自刷新,互不干扰。
代码如下:
class AddressChangedEvent { final AddressEntity address; AddressChangedEvent(this.address); } // 地址选择页 EventBus.getInstance().fire(AddressChangedEvent(newAddress)); // 结算页监听 EventBus.getInstance().on<AddressChangedEvent>().listen((event) { setState(() { _selectedAddress = event.address; _recalculateShippingFee(); }); });这里踩过一个坑:EventBus 的监听器在页面 dispose 时如果不手动取消订阅,会触发内存泄漏,而且在 OpenHarmony 上这个泄漏被 XTS 的内存检测直接标红。所以我在 mixin 里统一处理了订阅的生命周期。
3.3 下拉刷新与加载状态处理的细节
热词里还有个"flutter下拉刷新",看起来基础,但结算页的下拉刷新和普通列表页不一样。普通列表页刷新后重新拉一堆数据就行,结算页刷新后要重新计算整个金额链路,不能只刷新 UI。这里我建议把金额计算收敛到一个纯函数里,输入商品、地址、优惠券,输出金额明细,这样下拉刷新只是重新执行一遍纯函数,不容易出现状态错乱。
另外加载状态有一个跟 OpenHarmony 平台强相关的问题:在鸿蒙设备上,部分低端机内存资源比较紧张,Flutter 页面在后台被系统回收后,回到前台时状态可能丢失。如果你的结算页做到了"选完地址返回后金额自动刷新",就要格外注意这个场景。
我的做法是在PageStorageKey和 provider 层都做了快照,进入页面后先恢复快照再请求最新数据。这样哪怕系统杀掉了页面,用户回来时看到的也是旧的但完整的状态,而不是白屏或者金额为 0 的异常状态。
4. 订单结算核心流程落地:金额计算与支付调起
4.1 订单确认与金额计算的实现
金额计算是整个结算实现里最容易出 bug 的地方。我把它拆成三层:
- 商品级计算:每个 SKU 的单价、数量、小计;
- 订单级计算:商品总额、运费、优惠券抵扣、积分抵扣、平台补贴;
- 实付计算:订单总额 - 所有减免,再取两位小数。
每一层都用不可变对象表示,计算过程用 Dart 的decimal包来做,避免浮点数精度问题。这一步在你做 Flutter 商城时就应该养成习惯,金额永远不要用 double。
订单确认接口返回的数据结构大概是:
{ "orderId": "20250214001", "totalAmount": "199.90", "freightAmount": "0.00", "discountAmount": "20.00", "payAmount": "179.90" }我建议 Flutter 侧拿到这个 JSON 后,不要直接渲染,而是先转成OrderAmountModel,用toStringAsFixed(2)控制小数位,避免出现179.9这种显示问题。
4.2 地址选择与 PlatformView 桥接
地址选择页在纯 Flutter 页面里很好做,但如果你的项目在 OpenHarmony 上要调用系统地图来展示收货地址位置,就必须通过PlatformView把原生地图视图嵌进 Flutter 页面里。
这一步在 OpenHarmony 上的实现要点是:原生侧用PlatformView的 OpenHarmony 适配版注册一个 view type,Flutter 侧通过UiKitView来加载。如果你的地图 SDK 没有鸿蒙适配版,也可以退而求其次,让 Flutter 侧打开一个原生页面来选点,选完把经纬度和详细地址回传给 Flutter。我实际项目里用的是后者,因为这样就不用处理 PlatformView 在滚动场景下的手势冲突了。
如果你确实需要 PlatformView,我提一个避坑点:在 OpenHarmony 的 Flutter 适配版里,PlatformView 的叠加层层级偶尔会盖住 Flutter 的弹窗。我在优惠券弹窗出现时,就发现地图视图会穿透到弹窗上层。解决办法是在弹窗显示前把地图容器切到Visibility.GONE,弹窗关闭后再恢复。
4.3 支付方式的动态调起与结果回调
支付调起是结算实现里最关键的一步。在 Flutter 层,我封装了一个PaymentService,对外只暴露pay(OrderEntity order, PayMethod method)方法。内部按支付方式分发到不同的 MethodChannel 实现:
class PaymentService { static const _channel = MethodChannel('com.example.market/pay'); static Future<PayResult> pay(OrderEntity order, PayMethod method) async { try { final result = await _channel.invokeMapMethod('pay', { 'orderId': order.orderId, 'payAmount': order.payAmount, 'method': method.name, }); return PayResult.fromMap(result); } on PlatformException catch (e) { // 这里要区分是用户取消、支付失败还是网络异常 return PayResult.failure(e.code, e.message); } } }在原生侧(OpenHarmony 的 ArkTS 代码里),对应的 MethodChannel 实现要处理几件事:先把 Flutter 传来的订单参数转成原生支付 SDK 能识别的结构,再调起支付 SDK,最后把支付结果同步回 Flutter。整个过程要注意异步线程的回调切换,MethodChannel 的结果一定要在主线程回传。
支付结果回传这里有个很关键的细节:不要在 await 支付结果的时候让用户停留在结算页干等。我的做法是调起支付成功后,结算页立刻进入一个"等待结果"的过渡状态,同时开启一个 60 秒的超时计时器。如果超时还没收到回调,就提示用户"支付结果确认中,请稍后在订单列表查看"。这样即使用户切到支付宝/微信又切回来,也不会出现页面卡死。
5. 踩坑实录:从 Impeller 渲染到异步回调的那些坑
5.1 Impeller 在 OpenHarmony 上的表现
热词里的"flutter impeller"值得单独说。Flutter 3.7 之后默认在部分平台开启 Impeller 渲染引擎,它的优势是避免了 Skia 的绘制命令堆积,在动画场景下更流畅。但 OpenHarmony 适配版对 Impeller 的支持,在一段时间内还不算完善。我在结算页的确认弹窗动画和页面转场上实测,如果开了 Impeller,偶发会有帧撕裂或者闪黑屏。
当时排查下来,问题出在 OpenHarmony 的图形栈和 Impeller 的 Vulkan 后端兼容性不足。如果你们的测试机是较老的鸿蒙设备,或者图形驱动版本偏低,建议先切回 Skia 引擎,在AndroidManifest.xml或 OpenHarmony 的 module 配置里关掉 Impeller:
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />OpenHarmony 上对应的配置是在module.json5里设置 Flutter 容器的参数。等你把结算流程完全调通、有富余时间后再开 Impeller 做性能对比也不迟。
5.2 Future 回调与微任务队列的坑
热词里有一条很专业的:"flutter future的then回调 是放入微任务队列吗"。答案是肯定的,then回调在 Dart 里确实是调度到微任务队列,而不是宏任务队列。这个知识点在普通 App 里可能没那么重要,但在结算这种有严格时序的流程里,它会导致一个问题:
如果你在支付成功后,在then回调里立刻调用订单详情页的跳转,而这个跳转依赖网络请求的结果,就会出现竞态。因为微任务队列的执行时机早于下一帧渲染,也可能早于某些原生回调的落地。我当时就遇到过:支付成功回调回来后,订单详情页拉取最新订单状态时,服务端还没最终确认支付成功,导致详情页显示"待支付"。
解决办法是在支付成功回调里加一个 800 毫秒的防抖,或者在跳转详情页之前对上拉刷新逻辑做轮询。听起来很蠢,但真实场景里这是最实用的方案。你也可以在服务端把订单状态改成"支付中",前端以预支付单的状态作为兜底,而不是强行依赖首次查询结果。
5.3 异常处理的统一封装
结算流程里可能出现的异常非常多:网络超时、支付取消、支付重复回调、库存不足、优惠券过期、地址无效。这些异常如果散落在各个页面里处理,代码会非常乱。我在项目里统一封装了一个CheckoutException类型,并做了分类:
| 异常类型 | 触发场景 | 用户提示 |
|---|---|---|
| NetworkTimeout | 请求订单确认超时 | "网络不给力,请重试" |
| PayCancelled | 用户在支付面板取消 | "您已取消支付" |
| PayDuplicate | 支付回调重复进入 | "支付结果确认中,请稍候" |
| StockChanged | 提交订单时库存变化 | "部分商品库存不足,已更新" |
| CouponInvalid | 优惠券过期或金额不满足 | "优惠券不可用,已为您切换最优方案" |
所有异常统一由CheckoutBloc捕获,再通过页面的StreamBuilder渲染成不同的 UI 状态。这样做的好处是,页面侧不需要关心异常从哪来,只关心当前处于什么状态。OpenHarmony 上跑弱网测试时,这个统一处理帮我省了大量排查时间。
6. 结算体验的细节打磨与上线前检查清单
6.1 极端场景测试清单
结算功能开发完成后,我习惯性做一轮"极端场景测试",这些场景在普通开发流程里很少被关注到,但上线后最容易出问题:
- 支付过程中杀掉 App 进程,重新打开后订单状态是否正确恢复;
- 快速点击两次"提交订单",是否生成重复订单;
- 切换支付方式瞬间锁屏,解锁后页面状态是否错乱;
- 在弱网环境下,地址列表加载一半时点击"提交订单",是否有兜底提示;
- 使用系统返回键从支付面板返回结算页,金额是否有变化。
每一项都需要在 OpenHarmony 真机上验证,不能只在模拟器里测。模拟器对支付 SDK 的支持很不完整,尤其是拉起外部支付 App 那一步,压根走不通。
这里我给一个通用经验:把"提交订单"按钮做成带状态机的按钮组件。空闲状态可点击、请求中显示 loading、成功进入下一步、失败恢复可点击。这样能避免很多重复点击和误触问题。
6.2 上线前检查清单
最后分享一份我每次上线前都会过的检查清单,是这次 Flutter for OpenHarmony 商城App 结算实现沉淀下来的:
- 确认 Flutter 的 release 包已开启混淆和压缩,HAP 体积是否在可接受范围;
- 核对 XTS 认证跑分记录,确认没有 fatal 级别的稳定性问题;
- 用低端鸿蒙设备实机测试结算页的滑动帧率,至少保持 45 fps 以上;
- 检查支付回调的幂等性,同一笔订单重复回调不会产生重复入账;
- 确认权限声明最小化,结算页不需要的权限一律不申请。
我个人的体会是,Flutter 商城 App 在 OpenHarmony 上最难的不是 UI 适配,而是把原生生态的差异用一层稳定的抽象层隔离开,让业务代码只关心"用户点击了提交订单",而不关心背后是鸿蒙的支付组件还是别的系统能力。结算模块做完之后,我们对这个抽象层的信任度大幅提升,后面的订单列表、售后流程都是直接在它的基础上进行扩展,效率比从零开始高太多。
如果你也正打算在 OpenHarmony 上复刻一套 Flutter 商城,先把结算打通,你会比想象中更快看到整个 App 跑通的曙光。