前阵子我们项目组接到一个需求:基于 Flutter for OpenHarmony 技术栈,做一款运行在 OpenHarmony 生态设备上的电子合同签署 App,而且要求尽可能复用现有 Flutter 业务代码。项目跑完我最大的感触是,这事并没有想象中那么顺利,真正的难点不在 UI 设计,而在后端签名 API 的集成、平台差异的适配、以及各种“在 Android 上没问题、一到鸿蒙容器上就表现诡异”的坑。这篇文章就按我们的实际推进顺序梳理一遍,重点放在 API 集成实现这条主线上,环顾整个方案选型和落地过程,给想用 Flutter 在 OpenHarmony 环境中做业务型 App 的团队一个可参考的样例。
这个项目适合谁看?如果你已经有一些 Flutter 基础,同时对 OpenHarmony 的应用开发了解不深,想搞明白“Flutter 代码能不能直接跑到国产系统设备上”,或者正在做商务、法务、办公类应用,需要对接电子合同服务商的能力,那这篇文章应该能帮你少踩不少坑。我不讲空洞的概念,只讲我们实际上怎么拆业务、怎么定架构、怎么把合同列表、合同详情、手写签名、签署提交这些链路一步一步接起来。
1. 项目全貌与方案选型
1.1 电子合同签署App实际包含哪些东西
电子合同,听着好像就是个“签名”功能,真拆开之后涉及的模块一点都不少。按我们当时的需求清单,至少有这么几块:
- 合同管理:合同列表、合同详情、合同状态流转(待签、已签、已过期、已拒签等)
- 文件服务:合同 PDF 的在线预览、本地缓存、下载分享
- 签署能力:手写签名板、签名图片生成、签名摘要处理、签署位置定位
- 实名与意愿校验:短信验证码、人脸核身(可选)、签署意愿确认
- 通知中心:签署提醒、到期提醒、撤回通知
- 个人中心:账号信息、印章管理、证书状态、签署记录
如果是在 Android 或 iOS 平台上做这些模块,很多现成插件可以直接用。但问题是这次要跑在 OpenHarmony 容器上,部分热门 Flutter 插件没有对应的平台实现,这就决定了我们不能无脑套以前的项目结构,必须提前做技术选型评估。
1.2 为什么选 Flutter for OpenHarmony,而不是直接用 ArkUI 重写
这是项目最开始大家讨论比较激烈的一个点。当时摆在桌面上有两个方案:一是完全使用 OpenHarmony 原生 ArkUI 重写一版,二是使用社区目前比较活跃的 Flutter 适配方案,把 Flutter 引擎跑到 OpenHarmony 设备上。
关于 ArkUI 重写:成本高。我们原本在 Android、iOS 上已经有一套比较完善的 Flutter 业务代码,合同列表、PDF 渲染、手写板这些模块都有沉淀。如果全部重写,意味着三端各养一套代码,后续合同模板调整、签署逻辑升级,每次都要同步三个端,工作量直接翻倍。
而 Flutter for OpenHarmony 的适配路线,原理上相当于把 Flutter Engine 通过一个移植层跑在 OpenHarmony 的 ArkUI 容器里,上层 Dart 代码保持不变。这意味着我们大部分 UI 层、状态管理、网络封装能直接复用,只需要把跟平台强相关的能力(比如系统相机、文件权限、Toast 等)替换成适配层提供的实现。实际上跑下来,业务代码复用率大约能到 70% 到 80%,剩下 20% 主要是裁剪掉 Android/iOS 特有依赖,替换成 OpenHarmony 可用的方案。
还有一个现实原因:团队人力有限。与其带着前端团队去啃一套新 UI 框架,不如让熟手 Flutter 开发继续做业务,新系统相关的适配工作集中给一位熟悉 OpenHarmony 的同事处理,这样人员配置上更平滑。而且后续如果需要回退到 Android 版本,代码还保留着,不至于把家底全部押在一套新生态上。
1.3 整体架构分层设计
架构上我们没有做得太花哨,沿用传统的分层方式,只不过在原有 Flutter 工程结构上增加了一层“平台能力抽象”。关键的分层如下:
| 层级 | 职责 | 关键技术点 |
|---|---|---|
| UI 层 | 页面组件、状态管理 | Provider / Riverpod 管理页面状态 |
| 业务逻辑层 | 合同状态机、签署流程编排、业务规则 | 纯 Dart 逻辑,不依赖平台 API |
| 数据层 | 网络请求、缓存、本地存储 | Dio 封装 + 轻量数据库或本地文件 |
| API 网关层 | 请求封装、鉴权、接口签名、错误码归一 | 统一拦截器、Token 刷新策略 |
| 平台适配层 | 与 OpenHarmony 能力桥接 | 平台通道、方法调用适配、引擎差异抹平 |
这样设计的好处是,当 OpenHarmony 适配层的某些能力不完善时,可以只替换适配层实现,上层业务代码基本不动。比如合同文件下载功能,Android 上直接用某个插件管理下载,OpenHarmony 上我们换成了基于平台通道调用系统下载能力,业务层接口签名没变,页面的进度展示代码也完全复用。
2. 环境准备与工程搭建
2.1 开发环境清单与版本对齐
Flutter for OpenHarmony 的适配分支,跟原生 Flutter 官方稳定分支不太一样。我们第一次接触时吃了不少版本匹配的亏,所以环境准备这里值得单独说一句:别随便拿最新 Flutter stable 分支直接跑,很可能编译到一半发现引擎层和 OpenHarmony SDK 不匹配。
最终我们项目组统一的环境如下,供你参考:
- 操作系统:Windows 11 与 Ubuntu 20.04 双平台开发,日常主力用 Ubuntu
- IDE:DevEco Studio 用于 OpenHarmony 工程构建,VS Code 用于写 Dart 逻辑
- Flutter SDK:使用社区适配 OpenHarmony 的分支版本,当时用的适配分支版本对应 Flutter 3.x 中的某个稳定小版本
- OpenHarmony SDK:API 9 及以上(具体以你目标设备系统版本为准)
- JDK:OpenJDK 17
- 编译工具链:sdkmanager、hvigor 构建工具
这里特别提醒一点:适配分支迭代很快,第一次配置前一定先去仓库看 Release 说明,确认它支持你手头 OpenHarmony 设备的系统 API 版本。我们现场就有同事拿旧适配分支配新 API 版本设备,编译一直报缺少符号,浪费了大半天。
2.2 初始化工程与目录结构
初始化过程本身不复杂,基本还是 Flutter 那套流程。重点在于创建一个壳工程,这个壳工程是用 OpenHarmony 工程形态存在的,Flutter 模块以依赖形式挂进去。实际目录大致如下:
project_root/ ├── ohos/ # OpenHarmony 壳工程 │ ├── entry/src/main/ets # 原生入口与平台通道注册 │ ├── build-profile.json5 # 构建签名配置 │ └── hvigorfile.ts ├── lib/ # Dart 业务代码 │ ├── main.dart # 入口 │ ├── core/ # 网络层、路由、状态管理基类 │ ├── modules/ │ │ ├── contract/ # 合同列表、详情、状态卡片 │ │ ├── sign/ # 签名板、签署确认 │ │ └── user/ # 登录、实名、个人中心 │ └── services/ # 业务层服务封装 ├── pubspec.yaml └── assets/ # 静态资源我们当时犯过一个错误,把业务代码直接写在壳工程的 ets 目录下,想着一起编译,结果 Flutter 侧方法找不到入口,后来老老实实把 Dart 代码独立出来,壳工程通过依赖方式引入 Flutter Module,问题就解决了。这种结构也方便未来抽离开源。
2.3 依赖管理与插件选型策略
pubspec 里的依赖不能照搬 Android 版项目。部分 Flutter 插件在 OpenHarmony 上没有原生实现,运行时会报 MissingPluginException。我们当时的选型原则是:
- 纯 Dart 实现的库优先用,比如状态管理 Provider、路由 GoRouter、Dio 网络库、intl 本地化
- 依赖平台能力的库,先查是否有人做过 OpenHarmony 适配。比如 shared_preferences 有社区适配版本,image_picker 需要仔细看支持程度
- 拿不准的插件,用平台通道自己封装一层,保证业务层不被第三方库绑死
第三点尤其关键。签名板功能我们最开始想找现成 Flutter 库,试了其中一个手写签名库,在 Android 上正常,鸿蒙容器上画出来的笔迹容易丢点。后来干脆自己用 CustomPaint 实现了一套手写板,数据采集和图片生成都由自己控制,后续对接签名 API 反而更顺手。
3. API 集成实现核心细节
3.1 网络层统一封装与请求链路
电子合同类 App 的 API 集成,和普通资讯类 App 最大的区别在于:几乎每个接口都和敏感数据相关,请求链路里必须考虑身份凭证、时间戳、签名摘要、防重放这几个要素。所以网络层不能只是简单封装 Dio 就完事,要在拦截器层面把事情做透。
我们定义了一个 AppHttpClient,内部持有 Dio 实例,封装了这几个统一逻辑:
- 基础地址管理:测试环境、预发布环境、生产环境通过编译环境变量注入,不写死在前端代码里
- 公共参数注入:设备编号、应用版本号、时间戳、随机数
- 身份凭证:从安全存储中读取 accessToken,自动放入请求头
- 响应码归一:后端统一返回 code/message/data 结构,拦截器负责把业务码翻译成上层可识别的错误类型
- Token 过期重试:识别 401 码后,串行刷新 Token,重新放行原请求
代码大致是这样一个思路:
class AppHttpClient { late final Dio _dio; AppHttpClient() { _dio = Dio(BaseOptions( baseUrl: EnvConfig.apiBaseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 20), )); _dio.interceptors.add(AuthInterceptor()); _dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true)); _dio.interceptors.add(ErrorInterceptor()); } Future<Map<String, dynamic>> get(String path, {Map<String, dynamic>? params}) async { final response = await _dio.get(path, queryParameters: params); return _unwrap(response.data); } Future<Map<String, dynamic>> post(String path, {Map<String, dynamic>? data}) async { final response = await _dio.post(path, data: data); return _unwrap(response.data); } }这里要小心一个细节:OpenHarmony 容器上的 Dart 网络栈对超时的处理严格程度和 Android 原生线程调度有关。我们在真机上测试时发现,部分低端设备偶现连接超时,后来把 connectTimeout 从 8 秒调到 15 秒,并加了重试机制,才比较稳定。这个值不建议照抄,看你业务接口实际响应速度调整。
3.2 登录鉴权与 Token 续期方案
电子合同 App 的登录一般不是简单的账密登录,通常要过实名认证。我们的实现链路由两步组成:
- 第一步,账密登录拿到 authCode,这一步只代表用户在账号体系内通过校验
- 第二步,用 authCode 换取正式 accessToken 和 refreshToken,这一步会校验设备的绑定关系、用户实名校验状态
安全存储上,我们没有用普通 SharedPreferences 存 Token,而是通过平台通道把敏感信息写入系统级加密存储。OpenHarmony 提供的安全存储能力跟 Android Keystore 类似,我们封了一个 SecureStorage 工具:
class SecureStorage { static const _channel = MethodChannel('app.channel/secure_storage'); static Future<void> write(String key, String value) async { await _channel.invokeMethod('write', {'key': key, 'value': value}); } static Future<String?> read(String key) async { return await _channel.invokeMethod('read', {'key': key}); } }Token 续期这块用了一个比较实用的策略:拦截器里发现 401 时,用一个 AsyncLock 保证同时只有一个刷新请求在跑,其他请求进入等待队列,等新 Token 出来后自动重放。简单说,就是避免多个业务请求同时触发刷新,造成刷两次的两个 Token 互相覆盖。
说一下踩过的坑:OpenHarmony 的加密存储能力在 API 版本偏低的设备上,返回值可能是空。我们加了降级策略,低版本设备允许将 Token 写入受限权限文件目录,但提醒用户设备系统版本过低。实际上企业客户设备基本都能保持系统升级到指定版本,这个坑最后影响面不大。
3.3 合同文件上传与下载的实现要点
合同签署流程中,文件服务是绕不开的。整个链路大概是:
- 上传:用户本地做好一份草稿合同(PDF),调上传接口获取文件 ID
- 下载:合同发起方签署完,接收方要在 App 内预览 PDF
- 归档:签署完成后,服务端生成最终的合同归档版本
文件上传我们采用分片上传方案,因为 OpenHarmony 设备里面存的大体积 PDF 不少,一个几十 MB 的附件如果直接 binary 上传,失败率会很高。分片大小我们定为 2MB,每片上传后服务端返回合并凭证,最后再调 merge 接口触发服务端合并。
Future<String> uploadFile({required String path, required String fileName}) async { final file = File(path); final totalSize = await file.length(); const chunkSize = 2 * 1024 * 1024; int offset = 0; List<String> chunkIds = []; while (offset < totalSize) { final bytes = await file.readBytes(offset: offset, length: chunkSize); final chunkId = await _api.uploadChunk( fileName: fileName, fileBytes: bytes, ); chunkIds.add(chunkId); offset += chunkSize; } return await _api.mergeChunks(fileId: fileName, chunkIds: chunkIds); }这有一个非常实际的问题:Dio 默认的 MultipartFile 在内存中创建时,会直接把整个文件读进内存。像我们这种大文件场景,手机内存本来就不宽裕,所以上传分片时特意用了 onSendProgress 回调,并且手动控制每次只读一个 chunk 的字节,避免一次性加载整个文件导致的 OOM。关于断点续传我们暂时没做服务端支持,但如果你的服务商支持 Range 请求,这个逻辑可以往后扩展。
下载合同文件的时候,我们原来是直接下载到临时目录,再手动拷到应用文档目录。后来发现 OpenHarmony 对用户可见目录的权限管控和 Android 不完全一致,如果你用系统相册或者文件管理器打开下载目录,可能找不到你的合同文件。正确做法是走系统文件保存能力,经由平台通道把 PDF 写到用户可访问的公共目录,同时返回一个 URI。这块各版本 API 差异较大,建议你们在目标设备上提前验证。
3.4 电子签署 API 对接:签名数据如何提交
电子合同签署 API 的对接是整个项目的核心环节。这里需要注意,签署 API 和普通表单提交不是一回事,它一般要求你提交四个层面的数据:
- 签署位置信息:合同中的页码、坐标域、签署区尺寸
- 签名图形信息:手写签名位图转 Base64 后的数据
- 签署摘要:对原始合同 PDF 内容做摘要计算,防止正文被篡改
- 时间戳与意愿确认标识:用户确认签署动作的时间点与校验凭证
签名板模块我们自研后,生成的是一张透明背景的 PNG,白底会遮住合同原文,影响布局。所以签名数据采集时,注意把图片背景设为透明,压缩成 PNG 再转 Base64。
ByteData? byteData = await _signatureController.toImage(pixelRatio: 2.0); Uint8List pngBytes = byteData!.buffer.asUint8List(); String base64Str = base64Encode(pngBytes);然后调签署提交接口:
final res = await _api.submitSign( contractId: widget.contractId, pageNo: currentPage, signX: signX, signY: signY, signWidth: signWidth, signHeight: signHeight, signImageBase64: base64Str, digest: contractDigest, timestamp: serverTime, );关于签名摘要,我们一开始理解有偏差,以为前端自己计算摘要后传给后端就行。后来服务端同学提醒,摘要算法必须和服务端约定统一,否则你算出来的摘要跟服务端验签的摘要对不上。而且摘要计算不能基于整张图片,要按照服务端规定的字段拼接规则来做。最简单的对接方式是让服务端提供一个“获取待签摘要”的接口,前端传合同 ID 和用户 ID,服务端返回拼接后的摘要字符串,前端直接把它作为参数传给签署接口。这样做能避免两端摘要逻辑不一致的问题。
如果项目合规性要求比较高,可能还要考虑国密算法支持,比如用 SM3 替代 MD5 做摘要,用 SM2 做签名。这个一般要看合同服务商的后端能力。我们项目里因为服务端框架本身已经支持了国密体系,所以前端只需要按照它 SDK 文档约定的算法来生成前期签名数据即可,并不复杂,就是注意别用错 Base64 的编码格式,比如标准 Base64 和 URL-safe Base64 不要混。
4. 核心功能落地:从合同列表到签署完成
4.1 合同列表分页与状态管理设计
合同列表这类页面,业务逻辑本身门槛不高,但在 OpenHarmony 平台上做分页加载有个容易忽视的点:列表组件和 Flutter 默认的 ListView 在大量节点渲染时,性能表现和 Android 平台有明显差异。特别是合同卡片里带缩略图、状态标签、时间等多个组件的情况下,快速滑动时在低端设备上会有掉帧。建议直接使用 ListView.builder + 固定 item 高度,避免动态高度带来的重复测量。
分页我们采用传统的 page/pageSize 模型,下拉刷新重置第一页,触底加载下一页。这里有一个业务规范:电子合同的列表是有状态约束的,后端不会把所有合同一次性吐出来,要么按用户身份过滤,要么按签署状态过滤。所以前端在设计筛选标签时,请求参数里要带上 status 字段,别全量抓取再本地筛选,那样既消耗流量又容易出权限问题。
代码上我用了一个简单的分页状态类:
class ContractListController extends ChangeNotifier { int _page = 1; bool _hasMore = true; bool _loading = false; List<ContractModel> _items = []; Future<void> loadFirstPage() async { _page = 1; _hasMore = true; final data = await _api.getContracts(page: _page, pageSize: 20, status: _status); _items = data.items; _hasMore = data.hasMore; notifyListeners(); } Future<void> loadNextPage() async { if (!_hasMore || _loading) return; _page++; final data = await _api.getContracts(page: _page, pageSize: 20, status: _status); _items.addAll(data.items); _hasMore = data.hasMore; notifyListeners(); } }这里想提醒一点:OpenHarmony 上列表的滚动事件回调在快速滑动时可能出现偶发丢事件,如果你用 ScrollController 的 listener 来触发加载更多,建议加一个 200ms 的防抖,否则一次快速滑动可能触发多次网络请求,虽然不是致命的 bug,但是后端同学会看到大量重复请求。
4.2 合同详情页与 PDF 预览方案
合同详情页主要有两块内容:合同元信息展示和合同正文预览。合同正文是 PDF 格式,手机上最常见的预览方案是加载到 WebView 里渲染。Android 上 Flutter 官方的 webview_flutter 插件可以直接用,但 OpenHarmony 容器上,我们需要通过 MDC(Multi-Device Container)的 Web 组件来做预览。
实际做法是:在壳工程的 ets 页面里放一个 Web 组件,通过平台通道把合同文件的 URI、标题、页码等信息传过去,然后由原生侧加载 PDF。Flutter 侧只需要占据一个占位区域,等原生侧回调预览准备完毕后加载 WebView。
这里有一个实际体验问题:如果直接把 WebView 覆盖在 Flutter 视图上,触摸事件会被原生控件抢过去,导致 Flutter 的返回手势失效。我们处理办法是把 WebView 放进一个单独的全屏路由页面,打开合同预览时直接跳到原生页面,返回时再切回 Flutter 页面。从用户视角看,它就是一次普通页面跳转,但开发上省掉了很多手势争夺的麻烦。
PDF 预览的加载进度也需要处理。PDF 文件比较大的时候,Web 组件首次加载白屏时间会超过 3 秒。我们做了一个 loading 状态页,在原生侧监听加载完成事件后,通过回调通知 Flutter 关闭 loading 蒙层。千万别小看这个细节,用户如果在签署过程中看到白屏,第一反应就是重新打开 App,容易造成重复进件的问题。
4.3 手写签名板:从轨迹采集到图片生成
签名板是电子合同 App 的核心交互组件。自研方案总体分三步:
- 第一步,CustomPaint 绘制笔迹路径,记录每个点的时间戳和坐标
- 第二步,手指抬起时,把路径序列转换成 Canvas 绘图指令,生成透明背景 PNG
- 第三步,把签名图片压缩到服务端要求的分辨率范围,转 Base64 参与签署请求
签名的关键不只是图片大小,还有笔迹的平滑度。原始采集的坐标点如果不做处理,生成的线条会有明显折线感,尤其是 OpenHarmony 设备触摸采样率参差不齐。我们加了一个轻量级贝塞尔插值,在两个触摸点之间补出一段曲线,笔画观感会好很多。
final path = Path(); for (int i = 0; i < points.length - 1; i++) { final p0 = points[i]; final p2 = points[i + 1]; final p1 = Offset((p0.dx + p2.dx) / 2, (p0.dy + p2.dy) / 2); if (i == 0) { path.moveTo(p0.dx, p0.dy); } path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy); }还有签名板尺寸适配问题。OpenHarmony 平板和手机的屏幕比例差异很大,如果签名板宽度固定,平板上会出现两个手指宽度的签名区域,用户还需要挪动手腕。我们最后采用按屏幕宽度比例的动态尺寸:手机占宽 0.9,平板占宽 0.5,高度统一占宽比例。这个细节看起来小,但实际对签署体验提升非常明显。
4.4 签署确认流程与异常中断恢复
签署不是一个动作,而是一条流程。我们的流程是:
- 用户点击签署按钮
- 弹出确认弹窗,展示合同名称、签署时间、签署份数
- 输入短信验证码进行意愿校验
- 调签署提交接口
- 显示签署成功状态并刷新合同列表
这里最容易出事的环节是第 4 步。如果用户点击“确认签署”后网络闪断,前端超时重试,而后端其实已经成功落库了,那用户这边会看到一次失败提示,但合同实际已经签了。再点击签署时,后端返回“合同已完成签署”,用户会误以为系统有问题。
针对这个问题,我们做了一版幂等化处理:每次签署请求在进入流程时,先生成一个客户端请求流水号 requestId,带着这个流水号去调签署接口。后端如果检测到相同流水号已经处理过,就直接返回成功结果,不会重复签署。如果你们对接的合同服务商不支持这种幂等机制,至少要做一个本地草稿状态机:发起签署后先把本地状态置为“签署中”,网络失败后提供“继续签署”入口,而不是让用户重新走整个流程。
我们用 OpenHarmony 的分布式数据库能力做了一个轻量任务持久化,签署请求发出前,先把携带的参数序列化存入本地,收到成功回调后再删除记录。下次 App 启动时,扫描未完成的签署任务,弹窗提醒用户“您有一份合同签署尚未完成”,这样能在很大程度上降低断网重签的客诉量。
5. 常见问题排查与避坑清单
5.1 OpenHarmony 容器上 Flutter 运行问题速查
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 编译报错找不到某原生符号 | Flutter 适配分支与 OpenHarmony SDK 版本不匹配 | 检查适配分支 Release 说明,对齐版本再编译 |
| 运行后 UI 正常但调用某个插件直接崩 | 插件没有对应 OpenHarmony 平台实现 | 改用平台通道自封装,或用社区专门适配插件 |
| 页面偶现黑屏,杀掉重进恢复 | 引擎层内存回收或进度帧丢失 | 清理页面缓存、减少复杂动画、检查路由恢复逻辑 |
| 本地文件路径读不到 | OpenHarmony 文件路径规则与 Android 不同 | 不要硬编码路径,用平台通道获取系统目录 |
| WebView 手势与 Flutter 冲突 | 原生控件叠在 Flutter 视图上层 | 将 WebView 独立为原生全屏页,避免同屏叠放 |
| 签名图片生成后边缘锯齿 | 未开启抗锯齿或像素密度不足 | Canvas 绘制时设置 isAntiAlias,toImage 提高 pixelRatio |
5.2 API 集成中的高频问题与调试经验
API 集成这个过程,我们遇到的另一个大坑是签名摘要的计算时机。最开始我们放在用户点击“确认签署”的时候才去请求摘要,这样如果用户在签署预览页停留了较长时间,后端生成的摘要可能已经过期。后来参考了服务商文档,把摘要在进入签署页面的时候就提前拉取,同时做 5 分钟有效期的本地缓存,超时后再重新获取。这样既缩短了确认签署到提交的间隔,也兼容了用户在签名板上反复书写的情况。
调试过程中,最容易忽略的是 Base64 字符串的编码差异。Java 层默认的 Base64 和 Dart 的 base64Encode 出来的结果,在某些特殊字符的处理上可能是兼容的,但如果你在服务端用了 URL-safe Base64,前端就必须对应切换。我们当时因为签名图片 Base64 里出现 +、/ 字符,被服务端网关拦截过一次,后来统一改成 URL-safe 编码才解决。真机上调试时,建议字符签名接口的请求响应日志里把敏感字段做掩码处理,避免现场排查问题的时候泄露测试数据。
5.3 平台通道使用与生命周期管理
OpenHarmony 场景下,平台通道的注册和注销要特别关注页面生命周期。Android 上 Flutter 插件生命周期管理相对规范化,而嵌入式容器场景下,native 页面销毁时如果通道没注销,后续调用会报 MissingPluginException。我们做了一个简单通道管理器,在所有原生页面 onPageHide 时统一释放未完成通道调用:
class ChannelGuard { static int _channelToken = 0; static int acquire() => ++_channelToken; static void release(int token) { // 通知原生侧释放资源 MethodChannel('app.channel/guard').invokeMethod('release', {'token': token}); } }另外,调原生能力前先判断 isMethodImplemented,不然 API 版本低一点的设备上会直接抛异常。这一条越是老设备越重要。
5.4 性能与内存优化心得
OpenHarmony 容器跑 Flutter 引擎,内存开销比原生的 ArkUI 应用要高一些,我们在低内存设备上遇到过几次 OOM 前的卡顿。主要优化落在三个地方:
- 列表图片用缩略图,而不是将大图直接载入 cell
- 合同详情页的 WebView 进程在离开页面时主动销毁,而不是等系统回收
- 签名板手写轨迹存储用轻量点集,避免在内存里保留整个笔画的对象
合同列表的缩略图我们是请求服务端返回 200x280 规格的封面图,不再在客户端用高分辨率 PDF 渲染第一页。特别是列表里如果同时展示几十个合同,每个都渲染 PDF 封面,对低端设备的压力非常明显。
6. 写在最后的几点心得
整个项目做下来,如果让我总结一个最重要的经验:在 OpenHarmony 生态里做 Flutter 应用,第一优先级是做好平台能力边界清单,先把哪些插件能用、哪些不能用在纸上列清楚,再开始写业务代码,能省掉至少一周的无用功。
第二点心得是关于 API 集成的。电子合同这类业务,后端接口往往不是一套内部自研服务,而是接的第三方法律合规服务商的开放平台,所以接口文档需要逐字去对。我们遇到过服务商文档写明“签名图片 base64 传参”,实际上要求的是去掉头部的纯 base64 数据,流程图上没提,只有在联调的时候才能发现。所以建议你们拿到 API 文档后,先写一个小工具把每个接口的入参出参打出来,逐一核对字段格式,再往界面上接。
第三点是团队协作上的体会。Flutter 开发和 OpenHarmony 原生开发的同事需要共同维护一个能力映射表。举例来说,Flutter 侧一个请求文件保存的功能,在 Android 上就是一个插件调用,在 OpenHarmony 上却涉及原生页面跳转、权限申请、URI 转换、回调管理,整个链路复杂得多。我们每周例会都过一遍这个映射表,谁动了底层能力,另一方及时感知,避免双方接口假设不一致。
最后再分享一个后续可以扩展的方向:我们现在的签署流程还是在线联动的,所有签署行为都必须实时连接服务端。下一步准备做离线签署能力,在无网环境下用设备本地安全区域缓存待签合同和签名密钥,等联网后批量提交。这个能力在部分工厂、园区现场场景里很有价值,也是 OpenHarmony 这类系统在行业应用上的一个典型需求。如果有团队已经在做类似方向,欢迎多交流,这条路值得一起趟。