1. 项目背景dart_proffix_rest 是什么、Proffix ERP 能解决什么问题1.1 先说清楚 dart_proffix_rest 这个库dart_proffix_rest 是一个用 Dart 编写的 Proffix REST API 客户端封装库。Proffix 是瑞士一家老牌 ERP 厂商产品覆盖财务、CRM、物流、项目管理等多个模块在德语区中小企业里渗透率相当高。这套系统有个特点——API 设计得比较规整基于 REST 风格资源路径清晰认证流程标准但还是有几个绕不开的痛点接口数量多裸调 HTTP 请求的话光是维护 URL 拼接和鉴权头就够呛。返回的 JSON 结构嵌套深不同资源间有关联引用手动解析容易出错。企业对接往往涉及多账号、多环境需要灵活的客户端配置能力。dart_proffix_rest 做的事情就是把这些重复劳动封装起来提供统一的认证管理、资源路径构造、请求发送和响应解析入口。简单说它就是 Flutter 端连接 Proffix ERP 的一个标准化管道。我当时接手鸿蒙化这个任务时先冷静评估了一下这个库的体积——它本身很轻量几乎没有界面相关的东西核心就是网络请求封装。这意味着鸿蒙适配的主战场不在 UI而在网络层、异步模型和平台依赖这三块。1.2 Proffix ERP 的生态与移动端诉求Proffix ERP 在桌面端和服务器端能力很强但官方移动端体验一直比较薄弱。很多客户买了 Proffix 之后一线业务人员仓库管理员、外勤销售、项目实施人员都希望在手机上完成查库存、报工时、审批订单这类操作。传统做法是直接用 Proffix 后台的网页版但网页版在手机上的交互并不友好尤其是弱网环境下的响应速度很感人。企业内部一般是两条路要么基于原生的 iOS/Android 重写一套移动端要么基于 Flutter 这类跨端框架快速搭建。Flutter 在前者中的优势很明显——一套代码多端运行UI 一致性好生态也够成熟。我接触的不少 Proffix 集成项目最终都选择了 Flutter dart_proffix_rest 这个组合。因为 dart_proffix_rest 已经帮开发者处理掉了相当一部分 ERP 对接的脏活累活。但把所有业务迁移到鸿蒙设备上就需要先啃掉鸿蒙化的硬骨头。1.3 鸿蒙化适配到底难在哪先说清楚鸿蒙适配不是简简单单改几个依赖版本就能把 Flutter 工程跑起来的。和 React Native 快照式适配不同Flutter 的三方库适配要面对的是整个引擎层、插件通道、原生平台实现这三级结构的变化。dart_proffix_rest 这类纯 Dart 库本身对平台不敏感麻烦的是它依赖的底层包比如 http、crypto、shared_preferences 等在鸿蒙上的实现是否完整。具体难点我总结为三个层面难点层面具体表现影响程度依赖兼容性一些 Dart 包依赖 dart:io 的能力而 dart:io 在鸿蒙 Flutter 引擎上并非 100% 等价高插件通道差异MethodChannel / EventChannel 的注册方式在鸿蒙端有差异中原生环境差异HTTP 证书校验、网络权限、存储路径等行为与 Android 不完全一致高这三个问题几乎每个鸿蒙化 Flutter 项目都会遇到。接下来我就把整个适配的思路、步骤和踩坑过程完整展开。2. 鸿蒙化适配的整体思路与方案选型2.1 鸿蒙 Flutter 生态现状调研动手之前我花了两天时间调研鸿蒙上的 Flutter 支持现状。目前鸿蒙 Flutter 主要有两条路线一是官方 OpenHarmony 社区的 flutter_flutter 分支二是各厂商基于自己发行版做的适配。对于外部开发者来说常用的是通过 harmony 仓库替换 Flutter SDK 和相关插件。这个生态的特点是基础框架已经能跑通UI 渲染、手势事件、基本 Dart 运行时但因为生态还在快速迭代很多第三方包的 ohos 版本并不齐全。尤其是一些偏后台逻辑的库除非有大企业贡献否则基本没人主动做适配。我梳理了一下 dart_proffix_rest 的依赖树发现主要依赖基本集中在四个方向网络请求http/ dio、数据缓存shared_preferences、安全证书crypto、异步处理async。其中 crypto 基本是纯 Dart 实现问题不大shared_preferences 有官方 ohos 版本真正需要重点关注的是网络请求那一层。2.2 三大适配路线对比针对 dart_proffix_rest 的鸿蒙化我考虑过三条路线。路线一修改库源码把网络请求从 dart:io 换成鸿蒙原生请求通道具体方式是在鸿蒙平台分支启用 HarmonyOS 的 HTTP 接口通过 MethodChannel 调起 Java 层/ ArkTS 层的 HTTP 能力。这个方案适配力度最彻底对底层掌控力强但工作量最大——需要为每一个网络方法写 channel 实现的胶水代码。路线二用条件导入 平台分支替换网络层也就是在 Dart 层定义抽象接口通过条件导入conditional import区分 dart:io 实现和 ohos 实现。思路类似于 Flutter 官方推荐的多平台文件组织方式src/http_impl_stub.dart、src/http_impl_io.dart、src/http_impl_ohos.dart。这个方案很优雅改动可控也符合 Dart 生态的标准做法。不过需要库作者配合或者我们 fork 仓库后自行维护分支。路线三不改底层直接替换网络依赖比如把底层 HTTP 客户端从 dart:io 的实现替换成社区已经适配鸿蒙的封装如果存在的话再在库的构造函数里注入自定义 HttpClient。我最终选择了路线二和路线三结合的方案。理由是dart_proffix_rest 本身已经定义了客户端配置入口我们可以注入自己的http.Client无需动主干代码只在平台入口做适配。这样即使后续某个内测包行为有差异也能在不动业务层的情况下调整。2.3 最终采用的落地架构我搭建的最终架构可以用一句话概括保持 dart_proffix_rest 对外 API 不变通过条件导入 平台包注入把网络层、存储层、证书校验这三个平台相关模块替换成鸿蒙可用实现。具体分层如下业务层不变。业务代码里调用ProffixClient的方式与 Android/iOS 完全一致。API 封装层不变。dart_proffix_rest 的request、search、get等方法内部仍走抽象Client接口。平台适配层新增。针对鸿蒙实现HttpClient的注入和证书校验逻辑必要时通过MethodChannel调用鸿蒙原生网络能力络。数据持久化层替换。把 shared_preferences 的引用改成 ohos 版本确保缓存逻辑在鸿蒙上可用。这套结构的优势是团队里其他同事切到鸿蒙分支开发时几乎不需要重新学习 API如果后续官方更新了 dart_proffix_rest我们只需要把差异合并回 fork 仓库即可。3. 核心改造步骤与关键代码实现3.1 依赖梳理与 ohos 版本确认拿到 dart_proffix_rest 工程后我最先做的一件事是把pubspec.yaml里的依赖全部列出来逐个比对是否有 ohos 对应的版本。这一步很容易被忽略但跳过的话后面编译报错了才来回头排查浪费的时间够写十个模块了。我建议这个环节一定要做文档记录方便后续排查和给团队里其他同事做同步。以我当时的工程为例依赖清单大体是这样的http核心网络客户端shared_preferencestoken 等轻量数据缓存cryptoHMAC 签名辅助path_provider日志目录、文件缓存目录intl日期格式化我当时的处理结论依赖包是否有 ohos 版本处理方式http是替换为鸿蒙适配版或在客户端注入阶段使用自定义 IOClientshared_preferences是直接替换crypto不需要纯 Dart保持原样path_provider是替换intl不需要纯 Dart保持原样然后改动 pubspec 中的依赖把普通依赖拆成多平台条件依赖在 dart_proffix_rest 的 fork 仓库里用flutter_ohos的环境标识做条件区分。如果你也在做类似适配建议养成一个习惯进入鸿蒙适配前先跑一条干净的基础工程验证 flutter 和 dart 的命令是否正常再在当前工程里逐步加依赖。不要一上来就全量编译很容易被堆在一起报错迷惑。3.2 网络层适配从 dart:io 到鸿蒙网络栈网络层是这次适配中最核心的工作。dart_proffix_rest 底层调用的是dart:io的HttpClient这个类在鸿蒙 Flutter 引擎里存在但实现细节和 Android 上不一样具体表现为对于不支持的 TLS 协议版本有概率静默失败对自签名证书和特定企业 CA 的处理方式不同部分 socket 选项和超时行为无法保证一致。稳妥起见我没有直接依赖 dart:io 的默认行为而是自己实现了一个基于 MethodChannel 的 HTTP 请求通道作为补充备份。这个通道的逻辑其实不复杂Dart 侧将请求参数method、headers、body传给鸿蒙侧鸿蒙侧的 ArkTS 代码使用 ohos.net.http 发起真实请求然后通过 MethodChannel 的回调把状态码、响应头、响应体传回 Dart。代码层面大致是这样class OhosHttpClient extends http.BaseClient { static const MethodChannel _channel MethodChannel(dart_proffix_rest/ohos_http); override Futurehttp.StreamedResponse sendBase(http.BaseRequest request) async { final bodyBytes await request.finalize().toBytes(); final responseMap await _channel.invokeMethod(request, { url: request.url.toString(), method: request.method, headers: request.headers, body: utf8.decode(bodyBytes, allowMalformed: true), }); return http.StreamedResponse( Stream.value(utf8.encode(responseMap[body] as String)), responseMap[status] as int, headers: (responseMap[headers] as Map?)?.cast(), ); } }这里你会注意到我把响应体统一转成了字符串再传回来这对 JSON API 完全够用同时也避免了二进制响应在通道传输时的编码坑。那么选择 MethodChannel 还是 EventChannel 呢3.3 平台通道打通EventChannel 与 MethodChannel 的用法差异在鸿蒙适配中dart_proffix_rest 没有用到持续性的推送或事件流所以大部分通信走 MethodChannel 就够了。但我在做授权过期自动重登时需要监听鸿蒙侧的网络状态变化和 token 失效信号这里就用到了 EventChannel。这两者的分工我总结过一张表Channel 类型适用场景数据流向鸿蒙实现的注意点MethodChannel一次性的方法调用通常是 Dart 到原生然后原生返回结果注意 invokeMethod 在异步回调中要确保返回值类型可被转成 MethodChannel 支持的类型EventChannel持续事件流通常是原生到 Dart 的主动推送鸿蒙侧需要通过事件源注册Dart 侧监听回调在页面销毁时记得取消监听否则会内存泄漏我在鸿蒙端写的事件通道实现大致是import { EventEmitter } from ohos/base; import { emitter } from kit.BasicServicesKit; let eventBridge: emitter.EventEmitter | null null; export function createNetworkEventChannel(channel: any) { channel.setEventSource((engine: any) { eventBridge engine; }); }然后在网络状态变更的监听回调里emitter.on(networkStateChanged, (data: emitter.EventData) { if (eventBridge) { eventBridge.sendEvent(networkStateChanged, data.data); } });Dart 侧接收static const EventChannel _eventChannel EventChannel(dart_proffix_rest/network_state); Streamdynamic watchNetworkState() { return _eventChannel.receiveBroadcastStream(); }这里有个实际经验值得分享鸿蒙的 EventChannel 在 Flutter engine 弱网或后台恢复的情况下偶尔会出现事件丢失或订阅延迟的情况。所以我在关键业务比如登录态失效通知上不会只依赖 EventChannel而是会加一层轮询兜底。具体做法是启动一个周期性任务每 30 秒通过 MethodChannel 查询一次 token 状态。虽然多了一些冗余请求但对 ERP 对接这种容错要求高的场景可靠性比省几个请求更重要。3.4 资源模型驱动的通用请求封装dart_proffix_rest 这种库的核心价值在于把 Proffix REST API 的资源路径和操作统一收敛。适配鸿蒙时这一块完全不需要动因为它是纯 Dart 的逻辑层只依赖网络客户端接口。我用到的封装模型大致是class ProffixResourceT { final String resourceName; final ProffixClient client; FutureT get(String id) { return client.getT(/$resourceName/$id); } FutureListT search({ MapString, dynamic? filters, int? limit, int? start, }) { final queryParameters String, dynamic{ if (filters ! null) filter: jsonEncode(filters), if (limit ! null) limit: limit, if (start ! null) start: start, }; return client.getListT( /$resourceName, queryParameters: queryParameters, ); } }在鸿蒙端使用起来感觉和 Android 端没什么区别。这也正是我们选择在底层做适配而不是在业务层改代码的原因——适配的层级越低对业务侵扰越小。不过有一点需要留意Proffix API 对分页、过滤条件的语法要求比较严格不同的资源可能有细微差异。这个跟鸿蒙适配无关但容易和适配问题混在一起排查的时候容易被带偏思路。我建议在对接每个资源前先用 Postman 或命令行直接调 Proffix API 验证请求格式再回到 Flutter 代码里做对接。4. 对接 Proffix ERP 的实战要点4.1 认证流程与令牌管理dart_proffix_rest 的认证流程基于 Proffix 的 token 机制大体是先调用 login 接口拿到令牌然后在后续请求的 header 里携带。这个过程在鸿蒙端的适配有一个隐患Proffix 的 token 有时效性通常几十分钟到几小时不等。如果网络层和存储层不一致可能导致登录态丢失用户被迫反复登录体验非常糟糕。我在鸿蒙端做 token 管理时设计了以下策略token 存储优先使用鸿蒙安全的本地存储而不是裸文件缓存。token 刷新时机在每次 HTTP 请求的拦截器里检查 token 是否接近过期提前刷新。离线模式如果检测到无网络直接抛业务异常而不是走无效请求。具体到 dart_proffix_rest我是在注入http.Client时包了一层拦截器。这一层拦截器在鸿蒙和 Android 上都复用了同一套逻辑只是底层存储实现不同。4.2 资源映射与数据模型设计Proffix ERP 的资源体系复杂动辄数百个资源对象。如果直接把所有资源一次性映射到 Dart model工程量和维护成本都极高。我的做法是按业务场景拆分子模块只映射当前移动端会用到的资源。比如移动办公自动化的第一版我只映射了四个资源Auftrag销售订单Adresse客户主数据Lagerbestand库存Zeiterfassung工时记录每个资源对应一个 Dart model通过fromJson/toJson标准化序列化。模型里只保留移动端 UI 需要的字段不把 ERP 里的所有字段都搬进来不够的字段后续按需补。这样设计还有一个好处在鸿蒙化的适配阶段要验证的资源范围很小问题排查起来很快。如果一上来就要映射两百个资源任何一个字段解析异常都会让适配进度受阻。4.3 离线缓存、自动重试与移动办公自动化场景移动办公自动化这个词听起来很大落到具体功能上其实就是几个核心场景的闭环外勤销售查库存、下单仓库人员扫码盘点、入库主管在移动端审批订单和请假项目实施人员现场报工时这些场景里最影响可用性的就是网络波动。仓库和施工现场的 Wi-Fi 经常不稳定我实测网络延迟能从 20ms 直接掉到 3000ms甚至直接断连。所以我在适配层里加入了两个关键组件离线缓存队列每次业务操作比如提交订单先进入一个本地队列在确认到达服务器后才标记为完成如果请求失败会自动将操作存入本地数据库等网络恢复后按序重放。这个场景的实现原理不复杂核心代码逻辑是这样的class OfflineTaskQueue { final QueueOfflineTask _tasks Queue(); Futurevoid enqueue(OfflineTask task) async { await _localDb.insert(task); _tasks.add(task); _process(); } Futurevoid _process() async { while (_tasks.isNotEmpty) { final task _tasks.first; try { await _perform(task); await _localDb.delete(task.id); _tasks.removeFirst(); } catch (e) { // 网络失败时停在队列头下一次调用 _process 时重试 break; } } } }请求重试策略对幂等的 GET 请求我设置了 3 次重试、退避时间从 500ms 开始成倍增长对非幂等的 POST 请求则只重试网络层错误不重试业务错误避免重复提交。FutureT _withRetryT(FutureT Function() request, {int maxRetries 3}) async { var attempt 0; while (true) { try { return await request(); } catch (e) { attempt; if (attempt maxRetries) { rethrow; } final delay Duration(milliseconds: 500 * (1 (attempt - 1))); await Future.delayed(delay); } } }当时实装这套逻辑后仓库同事用鸿蒙平板在弱网环境下提交入库单体验明显稳定很多。这也是我个人认为整个鸿蒙化适配中实用价值最高的部分。5. 常见问题与排查技巧实录5.1 鸿蒙设备上网络请求静默失败我初期调试时遇到一个很隐蔽的坑请求发出去后Dart 侧既没有收到成功响应也没有抛出异常整个请求像消失了一样。排查过程如下第一步在 MethodChannel 的 invokeMethod 外层加了日志确认请求确实到达了鸿蒙侧第二步在鸿蒙侧看看是不是有应用权限缺失——结果发现需要检查module.json5里的网络权限配置第三步鸿蒙的 HTTP 接口在某些网络环境下对非标准端口会有限制需要确认 target 配置。最终定位到是权限配置问题。鸿蒙应用需要在module.json5里显式声明网络权限忘记加的话请求会在原生网络层被拦截且不会上报异常给 Flutter 层。这个问题也提醒我鸿蒙适配不同于 Android很多权限声明的习惯要重新建立不能直接沿用原来 Android 的配置直觉。5.2 EventChannel 回调收不到另一个常见问题是在鸿蒙上使用 EventChannel 时Dart 侧收不到事件。排查思路建议按照以下几个顺序来检查 EventChannel 的 name 是否两端一致确认鸿蒙侧的setEventSource是否有被调用确认 Dart 侧receiveBroadcastStream是否有被监听。我在排查时发现鸿蒙侧如果风火后事件源没有被正确挂载到 channel事件就发送不出去。解决方案是在 channel 初始化时立刻向 Dart 侧发送一条空事件确保链路已经建立。channel.setEventSource((engine: any) { eventBridge engine; // 发送空事件确保 Dart 侧订阅生效 eventBridge.sendEvent(networkStateChanged, { ready: true }); });5.3 三方包在 ohos 环境下编译失败鸿蒙 Flutter 工程的依赖编译阶段经常出现各种版本不匹配问题尤其是这些场景碰到得最多错误特征常见原因排查建议包找不到 flutter SDK环境变量指向了普通 Flutter 而不是 flutter_flutter 的鸿蒙分支检查 flutter 命令路径插件编译失败插件没有 ohos 目录或 pubspec 缺少 ohos 声明找 ohos 版本或 fork 后补实现Gradle 相关错误鸿蒙工程构建体系与 Android Gradle 不完全一致确认构建工具链是否切换到 ohos 工具链这类问题的排查思路比较通用先单独编译一个最小依赖工程确认基础环境没问题再把业务依赖逐个加回去。我处理 dart_proffix_rest 依赖时也遇到过类似的情况后来是把所有依赖都固定到明确版本避免浮点版本导致每次拉取不一致。5.4 适配验证清单与经验总结我自己整理了一份适配自检清单每次升级或改动后都会过一遍检查项验证方式优先级登录 token 获取与存储断网重进 App 是否保持登录高网络请求基础功能GET、POST、搜索、分页高文件下载与上传导出 Excel、上传凭证图片中离线队列断点飞行模式开启/关闭场景中EventChannel 事件切换网络、token 过期通知中内存与稳定性长时间驻留后台再返回中这个清单在团队内部流转后其他同事做鸿蒙适配时也会沿用能减少很多重复踩坑。最后分享一点个人经验鸿蒙化适配容易走进一个误区就是总想着把代码改得更“原生”、更“彻底”结果越改越乱。我的实际体会是适配的价值在于维持业务层稳定同时让底层实现能够灵活替换。只要能保证 dart_proffix_rest 接口行为一致、数据和状态不丢底层用什么通道、怎么跟鸿蒙原生通信其实都可以灰度切换、逐步演进。我后来把这个项目里沉淀的方案整理成了几个独立模块团队里新接手的人照着流程走两三天就能跑通一个最小闭环。这也是我认为鸿蒙化适配最有意义的部分——不只是让代码跑起来而是让后来的同事少踩坑。