把dart_proffix_rest这个库真正跑到鸿蒙系统上,我前后花了大概两周,踩的坑比想象中多得多。这东西是Flutter生态里对接Proffix ERP的REST客户端库,Proffix ERP在德语区制造和贸易企业里用得相当广,API设计得很规范,字段级操作、文档流、权限模型都齐全。刚拿到需求时,我第一反应是“应该不难”,毕竟dart层是纯Dart逻辑,理论上鸿蒙Flutter引擎能直接跑——但“理论上”这三个字,往往就是坑的开始。这篇文章把整个适配过程、方案选型、平台通道处理和排错记录完整写出来,给准备做Flutter三方库鸿蒙化适配的团队一个参考。
1. 为什么要做鸿蒙化适配:不是赶时髦,是业务侧的硬需求
1.1 企业移动办公的真实痛点
我们对接的这家客户是做工业设备分销的,销售、仓库、售后工程师加起来三百多人,日常业务高度依赖Proffix ERP:查库存、下订单、扫序列号、走审批流。过去这些人手里清一色Android手机,公司统一配发,倒也没什么问题。但从去年开始,越来越多的员工自己换成了搭载鸿蒙系统的设备,尤其是销售岗和管理层,手里全是新款的华为手机和平板。
问题就出在这。企业微信、邮件、文档这些都能在鸿蒙上正常跑,但一打开我们的Proffix移动端App就闪退。因为当时那版App是Flutter写的,只编译了Android和iOS的注册表,鸿蒙设备根本加载不了libflutter.so对应的原生插件。员工怨声载道,IT部门天天被催,最后这个需求就压到了我头上。
1.2 三个备选方案,为什么选了Flutter插件适配
接到需求后,我第一件事不是打开IDE,而是先把方案理了一遍。当时摆在台面上的路径其实有三条:
| 方案 | 实现方式 | 成本 | 体验 | 后续维护 |
|---|---|---|---|---|
| A | 用ArkTS从零重写一套Proffix客户端 | 极高,Proffix接口几十个,所有页面重画 | 原生级体验 | 两套代码并行,成本翻倍 |
| B | 套壳WebView加载Proffix网页版 | 低,几天能出Demo | 移动端体验差,离线能力弱,摄像头扫描基本没法用 | 依赖网页版改动 |
| C | 保留Flutter业务代码,补全鸿蒙平台侧适配 | 中等,集中在平台插件和依赖替换 | 与Android/iOS一致 | 一套Dart代码多端复用 |
方案A直接被我否了,不是技术做不到,而是成本上不划算。我们光业务页面就有40多个,全用ArkTS重写,至少要两到三个月的纯开发时间,而且以后每次加一个Proffix字段,两边都得同步改,迟早出问题。
方案B看起来快,实际上后患无穷。Proffix的网页版在手机上交互并不好,尤其是仓库场景要用相机扫条码,WebView里调摄像头那套兼容性问题能把人折磨死。再加上移动办公不能完全依赖网络,离线的数据缓存、审批草稿上传这类需求,用WebView做起来极其别扭。
方案C是我们最终的选择,核心逻辑很简单:dart_proffix_rest这个库的Dart层绝大部分代码是平台无关的,它做的就是HTTP请求、JSON解析、数据映射这些事。鸿蒙系统本身已经跑通了Flutter引擎,我们真正要补的,只是那些依赖Android/iOS原生能力的插件链路,比如文件存储、安全存储、网络通道等。
1.3 不只是我们,跨端框架都在往鸿蒙走
决定做方案C之后,我专门去查了一圈行业动态,发现其实不只是Flutter,整个跨端开发的生态都在适配鸿蒙。Electron应用移植鸿蒙的教程已经有不少团队写过了,Tauri 2也有人跑通了鸿蒙后端,Flutter这边OpenHarmony SIG团队维护了自己的flutter_flutter和flutter_engine分支。像Okta这种做身份认证的Flutter插件,也有团队完成了鸿蒙适配流程并写成了文档。这说明什么?鸿蒙已经不是“要不要支持”的问题,而是“什么时候支持”的问题。我们这波不过是在这个趋势里先走一步而已。
2. dart_proffix_rest这个库,到底做了什么
2.1 Proffix REST API的调用模型
要适配一个库,先得搞清楚它背后对接的东西长什么样。Proffix ERP对外提供的是B1 REST API,整个调用模型设计得和SAP、Salesforce这类企业软件很像,但又更扁平化一些。
它有几个典型特征:
- 登录认证走POST接口,提交用户名和密码后,服务端返回一个会话凭证,后续请求都带着这个凭证走。
- 资源访问是统一的,客户、物料、订单、文档都通过类似
GET /api/b1/Adresse@AktuelleListe这样的方式拉取列表数据,@后面跟的是字段列表的别名。 - 字段系统非常灵活,除了标准字段还有大量自定义字段,API返回的JSON里以
@Type、@Id、@Nummer这类前缀标识元数据。 - 支持多语言,Proffix的界面是德语、法语、意大利语、英语都有的,API请求里会有语言参数控制返回的翻译文本。
dart_proffix_rest这个库,本质上就是把上面这套HTTP交互、会话管理、数据映射、异常处理封装成了Dart类。我翻了源码,它的核心组件大致是这几个:一个负责配置入口的管理器,用来设置服务器地址、租户ID、语言这些参数;一个负责发起请求和接收响应的客户端,内部封装了登录、鉴权、重试逻辑;还有一批数据读取和写入的工具方法,把Proffix返回的扁平JSON转成Dart对象。
2.2 鸿蒙化的分水岭:这个库依赖了什么原生能力
做任何三方库鸿蒙化之前,最重要的一件事就是先给它的依赖结构做个“体检”。我当时把pubspec.yaml拉出来逐行看,又把整个源码里出现的dart:ffi、MethodChannel、EventChannel、Platform.isAndroid这类平台相关的代码全搜了一遍。
结果如下:
- 网络层用的是纯Dart的
http包和socket实现,不涉及原生插件。 - JSON解析用的是Dart原生
dart:convert,也不涉及原生。 - 有没有涉及平台通道?实际源码里有一个很隐蔽的点,就是它在做证书校验和文件导出时,会通过
PathProvider去拿应用沙盒的路径,这部分就依赖了平台插件。
这就是典型的“分水岭”场景了:如果库本身是纯Dart写的,鸿蒙化几乎就是零成本,直接编译就能跑。但如果它默默依赖了path_provider、shared_preferences、flutter_secure_storage这类插件,那鸿蒙化就变成了一件事——找到对应的鸿蒙互补实现,并接入工程。
2.3 鸿蒙Flutter引擎的兼容性底子
OpenHarmony社区维护的Flutter分支做了不少底层工作,把Dart的dart:io、事件循环、GPU渲染引擎都移植到了鸿蒙的系统能力之上。Flutter的Impeller渲染方案在鸿蒙上也有人在做技术验证,不过我当时用的生产版本还是Skia方案,稳定性更稳妥一些。
这意味着在Dart侧,绝大多数纯逻辑代码是不需要改的。真正要动手的,是把原生侧的方法调用从Android的Java/Kotlin实现,换成鸿蒙的ArkTS实现,再把插件的注册方式从Gradle换成hvigor工程。所以与其说这是“兼容问题”,不如说这是“工程问题”更贴切。
3. 鸿蒙化适配实操全过程
3.1 环境准备:一套能编译hap包的工具链
工欲善其事,必先利其器。鸿蒙化第一步,是把编译环境搭起来。我当时基于OpenHarmony的Flutter分支做了整个开发环境,步骤整理出来大概是这样的:
- 安装DevEco Studio并配置好HarmonyOS SDK,这个是开发鸿蒙原生应用的基础。
- 拉取OpenHarmony SIG维护的flutter_flutter和flutter_engine分支,用来替代官方Flutter工具链。
- 配置环境变量时要把
flutter_ohos的路径放在PATH的最前面,因为鸿蒙的hap包必须由这个定制分支来构建。
这里有个非常容易踩的坑:环境变量弄混的时候,命令倒是也能执行,但构建出来的产物没有ohos平台目录,报错提示也比较隐晦,都是以“Target platform”为开头的描述。我当时卡了将近一个小时,最后排查下来就是环境变量优先级问题。
工程仓库建议用镜像源,国内直连官方源下载依赖包的速度不太稳定。这个和咱们的关系不大,纯粹是工程效率问题。
3.2 创建鸿蒙Flutter工程
环境就绪后,创建工程的方式和普通Flutter项目基本一样:
flutter create --platforms ohos proffix_mobile关键点是--platforms ohos,只有指定了这个参数,工程里才会生成ohos目录,包含鸿蒙侧的工程配置、权限声明、插件注册模板。
创建完以后,我做的第一件事是打开ohos目录下的module.json5,确认网络权限已经声明。Proffix是纯云端交互的ERP,没有网络权限什么都白搭:
{ "module": { "name": "entry", "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }权限这块漏了的话,编译不会报错,但运行期所有HTTP请求都会静默失败,日志里只能看到超时,排查起来特别难受。
3.3 依赖替换:把带原生实现的插件换成鸿蒙版本
工程创建好之后,重点就是依赖体检后的替换工作了。我整理了一张替换清单:
| 原依赖 | 作用 | 鸿蒙替代方案 |
|---|---|---|
| path_provider | 获取应用沙箱目录 | path_provider_ohos |
| shared_preferences | 轻量数据存储 | shared_preferences_ohos |
| flutter_secure_storage | 安全存储Token | 鸿蒙侧用自己的AccessToken能力封装或找社区Ohos实现 |
| dio(如果业务层用了) | 网络请求 | 纯Dart可直接跑,无需替换 |
当时我们业务代码里还用了sqflite做本地数据库,这个库在鸿蒙上也有对应的sqflite_ohos实现。替换逻辑很简单,把pubspec.yaml里对应的依赖换成鸿蒙版本,然后整理代码里的import别名。但要注意,这些Ohos版本的API和原版在接口命名上可能不完全一致。path_provider_ohos和原生版基本一致,但sqflite_ohos的初始化方式就略有差异,需要微调代码。
3.4 平台通道对接:MethodChannel和EventChannel的鸿蒙写法
如果说依赖替换是常规操作,那平台通道就是鸿蒙化适配里真正的技术核心。dart_proffix_rest在导出文件时用到了PathProvider取路径,这个还好处理。但我们自己的业务代码里,有一个地方需要通过原生能力把Token存到系统级的安全存储里,这个就必须自己写平台通道了。
Dart侧调用方式不变:
const platform = MethodChannel('ch.xx.proffix/secure_store'); final token = await platform.invokeMethod<String>('writeToken', { 'key': 'proffix_session', 'value': sessionToken, });鸿蒙侧的处理逻辑在ArkTS文件里实现。新建一个SecureStorePlugin.ets,注册实现MethodChannel的接收逻辑:
export class SecureStorePlugin { private secureStore: SecureStore; constructor() { this.secureStore = new SecureStore(); } onMethodCall(method: string, args: Record<string, Object>): Promise<Object> { if (method === 'writeToken') { return this.secureStore.setSync(args['key'] as string, args['value'] as string) .then(() => true); } return Promise.resolve(null); } }接着在ohos层把Channel注册到Flutter引擎上,这一步的代码通常放在Ability或者Entry对应的生命周期里:
flutterEngine.getPluginRegistry().register(new SecureStorePlugin());EventChannel也是一样的套路。我们有一个需求是让原生侧把后台下载任务的状态实时推给Flutter页面,当时用的就是EventChannel,在ArkTS侧实现Stream监听接口,把任务进度Process对象推给Dart侧:
EventChannel('ch.xx.proffix/download_progress') .receiveBroadcastStream() .listen((event) { // 更新进度条 });这里最关键的一点是:Dart侧的Channel名称必须和鸿蒙侧注册的完全一致,差一个字符都收不到消息,而且错误不会在编译期暴露,只会在运行时静默失败。别问我怎么知道的,问就是调了一下午。
3.5 编译hap包与真机调试
平台通道写完,就可以构建hap包了。命令相对简单:
flutter build hap --release第一次构建大概率不会顺利通过。我当时遇到的第一个报错是hvigor版本和Flutter分支要求的版本不一致,后来把DevEco Studio升级到指定版本才解决。多翻翻编译日志,OpenHarmony的报错信息已经做得比较友好,一般能直接定位到是哪个模块的问题。
真机调试时建议用官方推荐的方式:先用DevEco Studio把工程跑起来,然后通过flutter attach连接到设备上的Flutter实例。这样Dart层代码改动可以热重载,不用每次都重新构建hap包。鸿蒙设备上的调试流程比Android多了一个签名环节,测试机上要用自动签名,否则装不上App。
4. 企业级移动办公自动化场景实战
4.1 登录认证与会话管理
Proffix的登录认证在移动端做起来有几个细节。拿到用户名密码后,先调用REST登录接口拿会话凭证,这个凭证需要安全存储起来。我在鸿蒙端兜了一圈,最后还是选择了平台通道方案,通过ArkTS的SecureStore能力把Token存到系统级别安全区域,比直接写入SharedPreferences靠谱得多。
会话是会过期的。Proffix有会话超时机制,移动端需要在请求遇到401时自动走一遍“重登”流程。我这边用了一个非常朴素的拦截器模式:所有请求统一经过一个包装层,401响应统一拦截,刷新Token后重放原请求。这块逻辑在Dart层实现,鸿蒙化之后完全不用改动。
4.2 主数据查询与离线缓存
移动办公自动化的核心场景之一,就是网络不可靠时业务还能继续。仓库里扫码查物料,在地下室或者信号偏的地方,不能每次都转圈等网络。我的做法是把常用主数据(客户、物料、价格表)拉到本地做缓存。
缓存方案选了sqflite_ohos,数据库文件存在应用沙盒目录下,代码上和原来Android/iOS版本几乎没有区别。同步策略也很简单粗暴:每次App启动时检查数据版本号,版本不一致就全量拉取更新一次。Proffix的接口在数据量可控的情况下(几百个客户、几千个物料),全量同步也就几秒钟,完全能接受。
4.3 单据审批与业务流自动化
移动办公自动化最有价值的部分是审批流。dart_proffix_rest封装好了读取单据列表和变更状态的接口,我们的业务层在Dart侧定义了一套状态机:待审单从列表页拉下来,展示明细,审批通过就调Proffix的变更接口推进状态,驳回则写备注并回退。
这一整条流程在鸿蒙端跑通后,效果立竿见影。原来销售在电脑前才能处理的订单审批,现在坐地铁就能完成,整个决策周期以肉眼可见的速度缩短。领导层对这个落地效果是非常满意的。
4.4 页面架构与导航设计
移动办公App的页面结构不复杂,底部导航栏四个Tab:首页摘要、业务列表、审批中心、个人中心。鸿蒙上Flutter的导航框架和组件通信机制与Android并没有差别,热词里提到的flutter组件通信、flutter navigator切换页面后是否会丢失状态这些问题,在鸿蒙Flutter引擎上的行为和官方Flutter完全一致,不需要特殊处理。使用IndexedStack保留页面状态,或使用Navigator的StatefulShellRoute,哪一种方案在鸿蒙上都正常工作。
唯一注意的是,如果页面里嵌了原生视图(比如某些型号设备上要调用系统级的扫码头),就需要用PlatformView机制。鸿蒙Flutter引擎对PlatformView的支持在不断完善,我用的版本上运行稳定,但建议在适配时先做最小Demo验证,再集成到业务页面中。
5. 常见问题与排查技巧实录
5.1 编译期问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| flutter build hap报“Target platform not found” | flutter命令用的是官方分支,不是Ohos分支 | 检查PATH环境变量,确认flutter_ohos在最前面 |
| hvigor编译报版本冲突 | DevEco Studio版本与Flutter分支所需版本不匹配 | 按OpenHarmony文档要求锁定DevEco版本 |
| 第三方插件找不到ohos实现 | 插件本身未适配鸿蒙 | 找Ohos替代包,或者自己写平台通道 |
| 构建hap后安装失败 | 签名配置不对 | DevEco Studio里做自动签名 |
5.2 运行期问题排查
运行期最常见的问题就是“网络请求超时”。以前在Android上我会直接开抓包,鸿蒙上抓包方式略有区别但原理相通,用Charles这类工具做代理,然后在系统网络设置里配置代理和证书即可。注意鸿蒙的证书信任机制和Android不太一样,如果App里做了证书校验,抓包时会出现证书错误,需要临时关闭校验或者把抓包证书装到系统信任区。
另一个我遇到比较多的坑是字符编码问题。Proffix的德语区数据里有很多umlaut字符,比如ä、ö、ü,API返回的JSON必须按UTF-8解析,否则页面上全是乱码。dart_proffix_rest的响应处理默认没做编码检测,我在适配时加了一步处理,强制以UTF-8解码响应体。
5.3 平台通道消息丢失问题
鸿蒙上平台通道的调试比Android要困难一些,因为原生侧日志和Dart侧日志是分开的。我的调试思路是两头打点:Dart侧在invokeMethod前打印参数,鸿蒙侧onMethodCall入口打印方法名和参数。如果Dart侧日志显示已发送,但原生侧没有收到,基本就是Channel名称不一致或者注册时机不对。
还有一个容易被忽略的点:鸿蒙Flutter引擎的插件注册时机必须放在引擎启动早期,如果放在Ability的onWindowStageLoad之后,很可能错过Flutter页面发送第一条平台消息。我们当时用一个比较粗暴的办法,在入口Ability的onCreate里就完成插件注册,实测这个时机最稳妥。
5.4 给准备做适配的团队几条经验
第一,动手前务必做依赖体检,把pubspec.yaml里面所有传递依赖都翻一遍,区分“纯Dart包”和“带原生实现的包”,这个决定整个适配工作量的大小。
第二,不要一上来就追求所有功能全量适配。先跑通最小闭环:登录、拉取一条列表数据、展示在页面上。这个目标达成后,后面的工作就是查漏补缺。
第三,善用社区力量。OpenHarmony的Flutter生态虽然不如Android/iOS成熟,但path_provider、sqflite这些常用库都有官方或社区维护的Ohos版本,遇到问题先搜“某插件+ohos”,大概率能找到现成方案。
第四,做好平台通道的命名规范。建议在Dart侧建一个constants文件统一管理所有Channel名称,这样既避免原生和Dart侧不一致,也给后续维护留了余地。
6. 踩过一次坑之后的体会
整个适配过程走完,我最大的体会是:鸿蒙化一个Flutter三方库,真正的重点不在Flutter层,而在工程体系和平台插件这套链路的打通。dart_proffix_rest这种偏纯Dart的库算是运气好的,如果碰上一个重度依赖原生能力的库,工作量会完全不一样。
个人经验是,先做依赖体检,再做最小闭环,最后才去补业务完整性,这个顺序千万不能乱。还想分享一个小技巧:flutter attach连着鸿蒙真机调试时,可以把Dart层的hot reload和DevEco的ArkTS层Debug两者结合使用,平台通道调试效率翻倍。
这次适配只是第一步。后续我们还在规划把推送能力、文档扫描、离线同步都补上,那时候会涉及更多鸿蒙系统的原生能力,但走通了这条路以后,再遇到类似需求,心里就有底了。