
Flutter 拿来做 OpenHarmony 应用这两年在开发者圈子里讨论热度一直没降过。真正落到业务场景的时候尤其是像电子合同签署这种重状态、重数据、重本地能力的项目坑和亮点都特别多。我最近在做的这个电子合同签署 App核心流程已经跑通这期单独把已签合同这个模块拆出来聊聊实现思路和踩坑记录希望能给同样在 Flutter OpenHarmony 上做业务开发的朋友一点参考。这个模块表面上看起来就是一个列表加一个详情实际上牵扯到本地数据库设计、合同状态机流转、PDF预览、签名信息回显、以及和后端的数据同步策略。文章会按架构拆解、数据层设计、列表页实现、详情页与文件预览、常见问题这几个维度展开适合已经跑通 Flutter for OpenHarmony 基础环境、准备开发实际业务功能的开发者阅读。如果是刚接触 Flutter 或者 OpenHarmony建议先把环境搭建和 Hello World 跑通再来看这篇。1. 项目背景与模块拆解1.1 为什么用 Flutter 做 OpenHarmony 应用先说结论Flutter 在 OpenHarmony 上的成熟度已经足够支撑常规业务 App 开发尤其是以界面为主、逻辑可控的中小型应用。电子合同签署 App 的核心诉求是跨端一致性和快速迭代天然匹配 Flutter 的优势。当前 OpenHarmony 应用开发主推的是 ArkTS ArkUI但 ArkTS 生态相对较新很多成熟的三方库还处于补齐阶段。而 Flutter 在 UI 组合、状态管理、动画、以及丰富的 pub.dev 生态方面领先不少。对于团队里已经有 Flutter 经验、或者需要 Android/iOS/OpenHarmony 三端统一交付的项目用 Flutter 跑在 OpenHarmony 上是一个非常现实的过渡方案。此外OpenHarmony 的 Flutter 适配flutter_ohos 分支一直在保持同步更新底层渲染已经能稳定跑在 rk3568 这类常见开发板以及部分商用设备上。实测下来普通列表页和详情页的帧率表现尚可复杂动画和大量图片加载场景需要做一定优化。1.2 已签合同模块的核心需求分析电子合同签署 App 的完整流程通常包括创建合同、发起签署、对方签署、查看状态、下载归档。已签合同模块面对的是状态机走到已完成终态的合同数据功能并不复杂但细节要求高。我梳理下来已签合同模块至少要覆盖以下需求合同列表展示包含合同编号、标题、签约双方、签署时间、合同金额、状态标签。状态筛选与搜索默认展示全部已签合同支持按合同编号或对方名称搜索。合同详情查看展示完整的合同信息包括双方信息、签署记录、时间戳。合同原文预览支持 PDF 或图片形式的合同内容查看。电子签名信息回显展示签名位置、签署人、签署时间以及验签状态。本地离线缓存弱网或断网时仍然可以查看已签合同。后端同步与数据一致性保证本地数据和服务端数据最终一致。这个模块最容易被低估的就是最后两条。很多开发者以为已签合同是只读数据不需要考虑同步问题但实际业务中用户换设备、服务端合同作废、或本地缓存损坏的情况都会出现必须提前设计好容错。1.3 功能边界与整体架构在设计阶段我明确了已签合同模块的边界它不做签署操作不做草稿管理只聚焦于查看已完成的签约结果。边界清晰的好处是状态流转的复杂度被隔离在合同创建和签署环节这个模块可以更从容地处理数据展示和离线能力。整体架构按 Flutter 项目惯用的分层结构组织界面层列表页、详情页、PDF预览页使用 Provider 做状态管理。业务层合同查询、状态筛选、数据同步的交互逻辑。数据层本地数据库sqflite缓存合同元数据文件系统缓存合同原文同时封装远端 API 用于增量同步。这种分层在 Flutter 项目里非常常见但它对 OpenHarmony 适配有一个额外的好处如果某个底层插件在 OpenHarmony 上有兼容问题可以尽量把影响控制在数据层不至于让 UI 层跟着返工。2. 环境准备与工程搭建2.1 Flutter for OpenHarmony 的环境配置要点不是所有 Flutter 版本都直接支持 OpenHarmony。官方 OpenHarmony 适配分支是独立的 flutter_ohos 仓库本地需要按照该分支重新编译 Flutter SDK或者在发布渠道下载对应的 SDK 包。我在实际配置时踩了不少坑这里把关键步骤梳理成清单下载 flutter_ohos 对应的 SDK 包解压到自定义目录比如 D:\flutter_ohos。配置环境变量 PATH指向 flutter_ohos/bin。这里提醒一句改完 PATH 之后必须新开终端窗口否则环境变量不会生效。这个坑太常见了尤其是用过 VS Code 的用户终端不重启flutter 命令永远指向旧 SDK。安装 DevEco Studio并设置 OpenHarmony SDK 路径。如果直接用命令行构建还需要配置 HOS_SDK_HOME 环境变量指向 DevEco 自带的 SDK 目录。真机调试需要打开开发者模式同时通过 hdc 工具连接设备。hdc 的用法与 adb 类似但命令细节有差异需要单独熟悉。这些步骤看着不多但每一步都可能出问题。尤其是 SDK 版本对应关系flutter_ohos 分支会随 OpenHarmony API 版本演进版本不匹配时构建会直接报错而且报错信息经常让人摸不着头脑。2.2 依赖选型数据库、状态管理、文件预览依赖选型是 Flutter 项目里最讲究的部分。在 OpenHarmony 平台上选择依赖时要遵守一个原则优先选择纯 Dart 实现或官方维护的插件其次再考虑对平台通道依赖较强的插件。我最终选定的依赖组合如下数据库sqflite。虽然 sqflite 在 Android 上走的是平台通道但 sqflite 的 OpenHarmony 适配已经比较成熟。后来看到一个开源项目使用了 sqflite_ohos 这类适配库说明社区也在持续跟进。本地缓存场景sqlite 基本是标配。状态管理Provider。原因是学习成本低、依赖简单对已有 Flutter 项目迁移友好。如果团队熟悉 Bloc 或 Riverpod也可以用但不必为了这个模块更换全局方案。文件路径path_provider。获取应用文档目录和缓存目录的标准方案OpenHarmony 适配也做得不错。PDF 预览pdfx 或 pdf_render。这块是踩坑重灾区后面单独展开讲。图片加载cached_network_image。配合本地文件缓存合同截图和头像等场景够用。需要特别注意的是不要为了追求功能全面引入过重的依赖。OpenHarmony 平台通道和 Android 不完全一致很多插件在 Android 上没问题到了 OpenHarmony 上就静默失败或者直接 crash。2.3 工程目录设计工程目录这块我沿用了 feature-first 的结构因为业务功能模块比较清晰lib/ core/ # 网络、数据库、常量、主题 features/ signed_contracts/ models/ # 合同数据模型 providers/ # 状态管理 repositories/ # 数据仓库 pages/ # 列表页、详情页 widgets/ # 合同卡片、签名信息组件 shared/ # 通用组件与工具这种目录结构的好处是后续新增待我签署或草稿箱模块时可以在 features 下平行扩展互不干扰。已签合同模块的代码只关注自己的数据来源和展示逻辑不会把项目变成一个大杂烩。3. 已签合同的数据层设计3.1 合同状态机与数据模型定义已签合同虽然只展示终态数据但列表页往往需要展示签署时间归档时间等字段。这些时间点需要在状态流转时就被记录而不是等合同变成已签后反推。因此数据模型必须完整覆盖状态机各阶段的关键字段。我在设计合同数据模型时采用了这样的结构class Contract { final String id; final String contractNo; final String title; final String partyA; final String partyB; final double amount; final ContractStatus status; final DateTime? signedAt; final DateTime? rejectedAt; final DateTime? expiredAt; final String? localPdfPath; final ListSignatureRecord signatures; final int serverVersion; // 用于同步冲突处理 }状态字段 status 用枚举表达避免魔法数字enum ContractStatus { draft, // 草稿 pending, // 待签署 signed, // 已签署业务终态之一 rejected, // 已拒绝终态 expired, // 已过期终态 }已签合同模块只从 signed 这个状态取数据。但保留 rejected 和 expired 状态的好处是将来首页做统计图表时可以直接复用。3.2 本地数据库表设计与迁移策略本地数据库我建了两张核心表contracts 表和 signatures 表。前者存合同主信息后者存签署记录一对多关系。建表语句大致如下CREATE TABLE contracts ( id TEXT PRIMARY KEY, contract_no TEXT NOT NULL, title TEXT NOT NULL, party_a TEXT, party_b TEXT, amount REAL, status INTEGER NOT NULL, signed_at INTEGER, rejected_at INTEGER, expired_at INTEGER, local_pdf_path TEXT, server_version INTEGER DEFAULT 0 ); CREATE TABLE signatures ( id TEXT PRIMARY KEY, contract_id TEXT NOT NULL, signer_name TEXT, signer_role TEXT, signed_at INTEGER, signature_image_path TEXT, cert_hash TEXT, verify_status INTEGER );时间字段我统一用毫秒级时间戳INTEGER存储展示时再转换成格式化字符串。这么做主要是为了方便排序和区间查询也避免不同 SQLite 版本对日期字符串的比较出现意外。数据库迁移策略也很重要。App 上线后模型一定会变我提前在数据库工具类里写了 onUpgrade 逻辑。每次版本变更时递增 version并在 onUpgrade 里执行 ALTER TABLE 或数据迁移。这个过程别看枯燥后面业务加字段时能省下大量排查时间。3.3 本地缓存与后端同步的取舍已签合同的核心价值就是随时能调出来看所以本地缓存不是可选项而是必须项。但缓存策略要设计得合理不能无脑全量缓存。我的同步策略分三层首次进入已签合同列表时先从后端拉取合同元数据列表写入本地 contracts 表。合同原文PDF/图片按需下载。用户点击查看详情时如果本地没有原文文件才触发下载并缓存到应用文档目录。这样避免一次性下载大量大文件把用户流量吃光。增量同步。每次进入模块或者下拉刷新时带上本地最大 serverVersion 请求后端增量数据后端返回变更记录后更新本地库。这套策略在弱网环境下的体验比较平滑。实测在没有网络的情况下列表页依然可以秒开详情页能看到上一次缓存过的合同原文。关于数据库和后端同步的关系我再多说一句。很多 Flutter 项目直接在 UI 层调后端接口数据库只做登录态缓存。但电子合同这种对数据可靠性要求高的业务本地数据库必须是业务数据的一等公民否则离线查看和异常恢复都无从谈起。4. 已签合同列表页实现4.1 列表页结构与状态筛选列表页是已签合同模块的门面。整体结构我采用了搜索栏 筛选标签 列表的组合。默认进来展示全部已签合同顶部搜索框支持按合同编号和对方名称模糊搜索筛选标签支持按时间区间近一周、近一月、全部快速过滤。代码结构上我抽了一个 SignedContractListPage 和对应的 SignedContractProvider。Provider 负责持有合同列表数据、加载状态、搜索关键字和筛选条件。页面通过 Consumer 监听数据变化并刷新。class SignedContractListPage extends StatelessWidget { override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(已签合同)), body: Column( children: [ SearchBar( onSearchChanged: (keyword) { context.readSignedContractProvider().filter(keyword); }, ), FilterTags(...), Expanded( child: ConsumerSignedContractProvider( builder: (context, provider, child) { if (provider.isLoading provider.contracts.isEmpty) { return LoadingView(); } return ListView.separated( itemCount: provider.contracts.length, itemBuilder: (context, index) { return ContractCard(contract: provider.contracts[index]); }, ); }, ), ), ], ), ); } }这里有个小细节搜索和筛选操作不能频繁触发数据库查询否则输入法每敲一个字都会卡一下。我给过滤逻辑加了一个 300ms 的防抖实际使用下来流畅度明显提升。4.2 合同卡片组件设计与签名状态展示合同卡片是列表页最核心的 UI 组件。我把它设计成一个独立的 ContractCard widget包含以下信息区域左上角合同标题单行省略完整标题在详情页看。右上角状态标签已签合同统一展示为已签署绿色标签。中段展示合同编号、签约双方、签约时间。底部展示合同金额和签署人数金额右对齐。状态标签我做了统一的样式封装不同状态对应不同颜色和文案。虽然这个模块只展示已签状态但组件层支持全部状态方便其他模块复用。签名状态在卡片上只展示一个摘要比如双方已签署具体签名详情放在详情页。这样做的好处是卡片信息密度适中用户扫一眼就能判断合同是否签完。4.3 下拉刷新与分页加载列表页必须支持下拉刷新这是用户的直觉操作。我使用 flutter_easyrefresh 的 RefreshIndicator 封装了刷新逻辑每次下拉触发增量同步接口同步完成后更新列表。分页加载这块我选择每页 20 条。滚动到底部时自动加载下一页。分页条件基于数据库的 LIMIT 和 OFFSET同时配合时间倒序排序。FutureListContract queryContracts({ required int limit, required int offset, String? keyword, DateTime? startTime, DateTime? endTime, }) async { final db await DatabaseHelper.instance.database; var where status ?; var args Object?[ContractStatus.signed.index]; if (keyword ! null keyword.isNotEmpty) { where AND (contract_no LIKE ? OR party_b LIKE ?); args.add(%$keyword%); args.add(%$keyword%); } if (startTime ! null endTime ! null) { where AND signed_at BETWEEN ? AND ?; args.add(startTime.millisecondsSinceEpoch); args.add(endTime.millisecondsSinceEpoch); } final rows await db.query( contracts, where: where, whereArgs: args, orderBy: signed_at DESC, limit: limit, offset: offset, ); return rows.map(Contract.fromMap).toList(); }分页加载在数据量小的时候看不出差别但合同数量达到几千条时全量加载会造成明显的卡顿和内存压力。所以即使现阶段用户量小我也建议把分页写好避免后期返工。5. 合同详情与原文预览实现5.1 详情页信息架构合同详情页承担了让用户确认这份合同签了什么、谁签的、什么时候签的的职责。信息架构我按从上到下的顺序组织顶部展示合同编号和签署状态。合同基本信息区标题、双方名称、合同金额、生效时间、到期时间。签署记录区以时间轴方式展示每一位签署人的名称、角色、签署时间和签名缩略图。原文件预览区默认展示合同 PDF 的预览或者提示用户点击查看。操作区提供分享合同下载PDF验真等操作按钮。操作按钮不要堆太多。我最终保留了三个高频操作预览原文、下载归档、验真。其他低频操作收入右上角菜单保持页面干净。5.2 PDF/图片合同预览实现PDF 预览是电子合同 App 里绕不开的功能。在 OpenHarmony 平台上这一块的兼容性要提前验证不同插件表现差异很大。我先试了 pdf_render它在 Android 上表现不错但迁移到 OpenHarmony 之后直接出现了平台通道找不到的错误。后来换成了 pdfx 插件并配合本地文件路径加载 PDF才跑通。具体流程是从数据库读取合同的 localPdfPath。如果本地文件不存在调用后端下载接口把 PDF 保存到 path_provider 提供的文档目录。将本地路径交给 pdfx 组件渲染。PdfDocument? document; try { final filePath await ensureContractPdf(contractId); document await PdfDocument.openFile(filePath); } catch (e) { // 处理文件下载失败或格式错误 }如果合同原文是图片格式直接使用 Image.file 加载本地缓存文件即可。需要注意的是OpenHarmony 对应用沙箱目录的访问权限管理比普通 Android 更严格文件路径不能写死必须通过 path_provider 获取。5.3 电子签名信息回显电子合同的核心特征是签名信息可追溯。在详情页里我用了时间轴组件展示签署记录。每一位签署人的记录包含签署人名称和角色甲方/乙方。签署时间精确到秒。签名图片缩略图点击可查看大图。验签状态显示验签通过或验签失败。验签状态的实现原理是签署时对合同原文做哈希计算并把哈希值连同签名图片一起保存在服务端。详情页展示时先从本地计算当前 PDF 的哈希再与签名记录中的哈希比对。如果一致说明合同原文在签署后未被篡改。这个验签逻辑虽然代码不复杂但它是电子合同公信力的基础。如果项目后续要过等保或合规审计这块能力是必备的。我在页面上专门放了验真按钮点击后弹窗展示验证结果和哈希摘要。6. 常见问题与排查实录6.1 依赖版本冲突与下载失败Flutter 项目依赖冲突是常态在 OpenHarmony 上更明显。最常见的问题是 flutter_ohos 分支与 pub.dev 部分插件的新版本不兼容尤其是依赖了较新 Dart SDK 特性的插件。我自己就遇到过 flutter 各个版本不对导致依赖包下不下来的情况。排查思路是这样的用 flutter doctor -v 检查当前 SDK 状态确认 flutter_ohos 分支没有被系统 Flutter 干扰。检查 pubspec.yaml 里的 sdk 约束确保 Dart SDK 版本范围和 flutter_ohos 匹配。执行 flutter clean 清除缓存再重新 flutter pub get。如果某个插件一直下载失败检查是否用了国内镜像源并确认镜像源与 OpenHarmony 构建链兼容。还有一个容易被忽视的点OpenHarmony 的 Flutter 工程在构建时会额外读取 oh-package.json5 和 build-profile.json5 这类 OpenHarmony 工程文件。如果这些文件里的依赖版本与 Flutter 插件冲突报错信息不会指向 Flutter而是指向原生构建环节。6.2 OpenHarmony 设备适配与 kernel 选择不少开发者在调试时会遇到openharmony 的 rk3568 有许多设备树到底咋选这类问题。这里要澄清一个概念设备树device tree的选择通常发生在系统镜像烧录和内核编译阶段对于应用开发者来说一般只需要拿到一个能正常启动的 OpenHarmony 系统镜像即可。我们在 rk3568 开发板上调试时直接使用厂商提供的标准镜像烧录完成后通过 hdc 连接设备安装 Flutter 构建产物。如果应用在真机上出现触控或显示异常优先排查的是屏幕分辨率适配和刷新率设置而不是设备树选择。当然如果你是系统集成方的角色那设备树选型就要根据具体硬件外设来决定屏幕型号对应 lcd 驱动节点触摸屏对应 input 设备节点网络模块对应以太网或 WiFi 节点。这个环节和应用开发的交集很小不必过分担心。6.3 中文显示、字体与图片加载问题中文显示在 Flutter 里通常是开箱即用的但在部分 OpenHarmony 设备上中文字体文件可能没有默认打包进系统。如果页面出现中文乱码或空白需要在应用里显式配置字体。我的做法是在 assets 里放一个中文字体文件并在 MaterialApp 的 theme 中配置 fontFamily。这样能保证不同设备上的中文渲染一致避免设备间字体差异导致界面错位。图片加载方面如果使用 cached_network_image 加载远端合同截图需要确认网络图片的缓存路径能正常写入。真机上偶尔遇到图片无法显示的问题多半是开发者没有在 module.json5 中配置网络权限或存储访问权限。OpenHarmony 的权限模型和 Android 类似但细节不同改权限后要重新签名安装不要只在代码里加了权限就以为生效。6.4 安全加固与代码混淆反编译 Flutter 应用比反编译原生应用容易拿到 Dart 层逻辑这一点被很多人忽略。网上那个反编译 flutter的热搜就能说明问题。如果电子合同类应用没有做任何加固攻击者可以直接从产物里提取 Dart 代码分析签名逻辑和验签算法。我的建议是敏感业务逻辑不要全放在 Flutter 侧。签名算法、合同哈希计算、证书校验这类安全敏感操作至少要做到Dart 层只做结果展示和基础判断。真正校验逻辑放在服务端或通过 OpenHarmony 侧的原生模块执行。对 Flutter 产物做加固处理增加逆向难度。关键接口做签名和防重放避免被抓包后模拟请求。这个模块虽然只是已签合同但因为它承载了合同原文和签名数据安全级别直接对标金融类应用。不要以为只有签署过程才需要安全防护合同归档后的泄露风险同样致命。7. 总结与后续扩展思路已签合同模块从数据模型设计到列表页、详情页、文件预览再到底层同步和适配调试整体走下来我对 Flutter for OpenHarmony 的实际业务支撑能力有了更具体的认知。它不是只能在 demo 里跑跑而是已经可以承载真实业务逻辑了但需要开发者在依赖选型和安全设计上多花心思。后面我计划在这个模块上继续扩展几个能力一是合同验真结果的二维码输出方便用户在 PC 端扫码核验二是增加合同归档导出功能把关联的签署记录和验签报告打包成一个压缩文件三是接入 OpenHarmony 的分享能力让用户可以直接把合同分享到其他应用。如果你也在做 Flutter for OpenHarmony 的电子合同或类似文档类应用建议优先把数据层的离线能力和文件缓存机制理清楚这是整个模块稳定性的地基。至于 UI 细节和动画效果可以后面慢慢打磨。