开工之前,先聊聊这套系统到底在解决什么问题
这两年做跨端应用,我最大的感受是“跨端”二字的含金量正在被重新定义。以前说跨端,基本就是一套 Flutter 代码跑 iOS 和 Android,顶多再兼顾一下 Web。但从去年开始,OpenHarmony 的设备量明显起来了,尤其是行业定制设备——工业平板、车载终端、维修门店的工位看板,很多都换成了基于 OpenHarmony 的方案。我们接到的车辆维修管理系统需求里,客户点名要求“一套代码,既要跑在原来的 Android 平板上,也要跑在 OpenHarmony 的 RK3568 工控机上”,这才是真正的跨端挑战。
这个项目里最有代表性、也最适合拿出来拆解的就是通知公告模块。原因很简单:它是维修管理系统里最高频、最容易被感知的功能——技师开工要看公告、接单要看派工通知、管理层下发制度要看推送。同时它又是典型的“数据展示 + 实时同步 + 多端适配”场景,正好能把 Flutter 的跨端能力、OpenHarmony 的平台特性、本地数据库与后端同步的常见矛盾全部串起来。
这篇文章我就以通知公告模块为切入点,完整复盘一下这套 Flutter × OpenHarmony 跨端车辆维修管理系统从设计到落地的过程。适合正在做 Flutter 跨端项目、准备适配 OpenHarmony、或者对“本地数据库 + 后端同步”方案感兴趣的开发者参考。我会把模块设计思路、数据库选型、同步协议、UI 实现、OpenHarmony 适配细节和踩坑记录都写清楚,尽量做到拿来就能用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
1. 整体架构与设计思路拆解
1.1 为什么是 Flutter + OpenHarmony,而不是别的组合
先回答一个很多人会问的问题:既然要适配 OpenHarmony,为什么不直接用 ArkUI 原生开发,非要绕一圈用 Flutter?
我的回答是:看你的存量代码和团队结构。我们团队从三年前就开始用 Flutter 做业务,维修管理系统里已经有大量成熟的 Flutter 业务组件、工具库和状态管理方案,如果全部用 ArkUI 重写,成本不是一个数量级的。而 Flutter 官方对 OpenHarmony 的适配已经比较成熟,Flutter 3.7 之后,社区维护的 flutter_flutter 分支可以跑在 OpenHarmony 3.2 及以上版本,核心的渲染引擎、Dart 运行时、Platform Channel 机制都能正常工作。这意味着我们只需要处理平台相关的桥接层,业务层代码几乎零改动。
从实际效果看,Flutter 的 Skia 渲染引擎在 RK3568 这类中低端芯片上表现也可接受。公告列表的滚动、图片懒加载、页面转场这些常规操作都能保持流畅,不会出现明显的掉帧。相比 ArkUI 的声明式开发,我们的团队迁移成本更低,开发效率更高。
1.2 通知公告模块的边界划分与功能定位
在开始写代码之前,我们花了很大精力做模块边界划分。通知公告模块看起来简单,但如果没有清晰的定义,很容易在后期被各种需求“加料”加到失控。
我们的做法是把这个模块拆成四个子功能域:
- 公告列表:分页展示、分类筛选(维修规范、安全通知、排班调整、客户提醒)、关键字搜索。
- 公告详情:富文本渲染、附件下载、阅读状态标记。
- 消息中心:与公告不同,消息是面向单个用户的,比如“您有一条新派工单”“您负责的工位设备需要保养”,展示维度是“与我相关”。
- 系统通知:强提醒类内容,比如版本升级、系统维护、紧急安全通知,支持推送和弹窗。
这四块的共性在于都是“服务端产生内容 → 客户端拉取/接收 → 本地存储 → 用户阅读/操作”,所以我们统一抽象了一套数据模型和存储方案。差异在于消息的个性化属性更重,公告的系统属性更重,体现在数据库表设计和同步策略上会有所不同。
1.3 技术选型的核心考量:离线优先还是在线优先
做维修管理系统,有一个场景必须考虑:维修车间的网络环境并不总是稳定的。地沟、烤漆房、车间角落,很多工位的位置信号不好,如果系统设计成“必须联网才能看公告”,一旦网络抖动,技师连今天的安全早会内容都看不到,这就很尴尬。
所以我们决定采用“离线优先”(Offline-First)的策略:
- 客户端内置本地数据库,作为公告和消息的“第一数据源”。
- 进入模块时,先展示本地缓存的数据,用户无感知等待。
- 后台静默同步,与服务端比对增量数据,有新公告就拉取,有阅读状态变更就上报。
- 网络不可用时,降级为纯本地模式,用户仍然可以浏览已缓存的公告,阅读操作也会记录在本地,待网络恢复后自动补交。
这个设计直接决定了后续的数据库选型和同步协议设计。它和普通 CMS 的公告系统最大的区别在于:我们不是简单地从接口拉数据展示,而是要做“本地库 + 远程库”的数据对账。
2. 本地数据层设计:内嵌数据库的选型与表结构
2.1 Flutter 内嵌数据库选型:sqflite 还是 drift 还是 Hive
数据层是通知公告模块的地基。Flutter 生态里常见的本地数据库方案我基本都试过,这里做一个对比:
- sqflite:老牌方案,基于 SQLite,SQL 语法完整,和 Android 原生开发的习惯一致。但在 OpenHarmony 上有兼容性问题,需要依赖社区适配包,且不支持跨 isolate 访问,复杂查询容易卡 UI。
- drift:基于 sqflite 的封装,提供类型安全的查询 API 和迁移工具,开发体验好很多,但底层还是依赖 sqflite,OpenHarmony 上同样存在适配问题。
- Hive:纯 Dart 实现的 NoSQL 数据库,读写速度极快,但只支持简单的 key-value 和对象存储,不适合做复杂的列表筛选和条件查询。公告模块有分类筛选、分页、排序这类需求,Hive 用起来会非常别扭。
- OpenHarmony 自带的 RelationalStore:这是 OpenHarmony 为应用提供的原生关系型数据库 API,基于 SQLite,支持复杂的 SQL 查询,性能稳定。Flutter 通过 Platform Channel 调用 RelationalStore 是一个完全可行的路径。
我们最终的选择是:Android 和 iOS 平台用 sqflite,OpenHarmony 平台用 RelationalStore,然后用一个统一的抽象层把两者的差异封装起来。为什么不用纯 Dart 方案?因为公告数据量不大,但查询模式复杂,SQL 关系型数据库在语义上最合适;为什么不做单一数据库?因为 Flutter 官方在 OpenHarmony 上的 sqflite 适配还不够完善,与其在兼容层上耗费精力,不如直接对接 RelationalStore 原生能力。
2.2 公告与消息的数据表设计
数据库表设计我直接放出最终版本,这是经过几个版本迭代后比较稳定的结构。
公告表(notice_tb):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | TEXT | 公告 ID,服务端 UUID,主键 |
| title | TEXT | 公告标题 |
| content | TEXT | 公告内容,富文本 JSON 或 HTML |
| category | INTEGER | 分类:1维修规范,2安全通知,3排班调整,4客户提醒 |
| priority | INTEGER | 优先级:0普通,1重要,2紧急 |
| publisher | TEXT | 发布人 |
| published_at | INTEGER | 发布时间,Unix 时间戳 |
| updated_at | INTEGER | 更新时间,用于增量同步 |
| is_read | INTEGER | 是否已读,0未读,1已读 |
| is_deleted | INTEGER | 软删除标记,同步时用于处理服务端删除 |
消息表(message_tb)与公告表结构类似,但增加了一个 receive_user_id 字段,用于标记这条消息是发给哪个用户的。另外消息表有 message_type 字段,区分派工类、设备类、系统类。
需要特别说明的是 is_deleted 这个字段。我们在实际开发中踩过一个坑:最开始设计时直接用了物理删除,服务端删掉一条公告,客户端同步时也删掉本地记录。结果有一次公告在服务端被误删,客户端同步后所有技师都看不到那条安全操作规范了,最后只能让服务端从备份里恢复数据。从那以后我们统一改成了软删除——服务端标记删除,客户端同步时更新 is_deleted = 1,查询时过滤掉,但数据仍然保留在本地。这样即使服务端误操作,本地数据还能兜底。
2.3 统一数据库访问层:如何屏蔽 sqflite 与 RelationalStore 的差异
选定双数据库方案之后,最大的工作量其实在抽象层设计。我建议不要在每个页面直接调用 SQL,而是定义一个 Repository 接口,上层业务只关心数据操作,不关心底层是 sqflite 还是 RelationalStore。
简单来说,我们在 Dart 层定义了一个 NoticeRepository 抽象类,核心方法包括:
Future<List<NoticeEntity>> queryNotices({category, page, pageSize})Future<void> upsertNotices(List<NoticeEntity> notices)Future<void> markAsRead(String id)Future<List<NoticeEntity>> queryUnreadNotices()
Android/iOS 平台的实现类内部使用 sqflite,OpenHarmony 平台的实现类通过 MethodChannel 调用原生 RelationalStore API。两边的 SQL 逻辑基本一致,只是数据库的连接方式和查询接口不同。
在 OpenHarmony 原生侧,我们需要编写一个轻量级的数据库操作类,接收来自 Flutter 的方法调用参数(比如表名、SQL 语句、占位参数),执行完后返回结果集。这里有个性能细节:不要为每一条查询都创建一个新的数据库连接,RelationalStore 的 getRdbStore 是重量级操作,应该在应用启动时初始化一次,之后复用同一个 store 实例。我们第一次适配时没注意这个问题,导致公告列表每次刷新都会卡顿几百毫秒,后来加上单例初始化才解决。
2.4 关于 RK3568 设备树选择的一个小插曲
热搜词里有个“openharmony 的 rk3568 有许多设备树到底咋选”,这确实是很多人会卡住的点。我们在做 OpenHarmony 真机调试时也遇到过:同一块 RK3568 开发板,官方系统里可能带了好几套设备树文件,选错了直接导致触摸屏失灵、网口不通、显示异常。实际经验是:优先选择与你的屏幕型号、触摸 IC 型号匹配的设备树,一般开发板厂商会给出明确的映射关系。最稳妥的做法是直接向板卡厂商的技术支持要适配好的系统镜像,而不是自己在多套设备树里猜。如果项目做的是行业定制设备,强烈建议把设备树适配工作交给硬件厂商或专门做 OpenHarmony BSP 的团队,应用层开发人员没必要在这上面花太多时间。
3. 服务端同步机制与增量拉取协议
3.1 为什么放弃 WebSocket 实时推送,改用轮询 + 增量拉取
通知公告模块一个很自然的做法是用 WebSocket 做实时推送,服务端有新公告就主动推给客户端,体验确实好。但我们最终放弃了这个方案,原因有三:
第一,OpenHarmony 工控机上跑的 App 经常处于弱网或离线环境,WebSocket 长连接在这种场景下会频繁断开、重连,耗电且不稳定。第二,维修门店的网络环境可能有多层 NAT,部分网络策略会阻断长连接。第三,公告类内容的实时性要求没那么高,允许存在几十秒的延迟,完全可以用定时刷新解决。
我们的最终方案是“短轮询 + 增量拉取”:客户端每隔 30 秒调一次同步接口(也可以手动下拉刷新),请求参数带上本地最新公告的更新时间(maxUpdatedAt),服务端只返回这个时间戳之后有变更的记录。这个方案的优点是实现简单、无状态、可靠,缺点是有一定的冗余请求,但公告模块的查询压力很小,完全在可接受范围内。
如果你有非常紧急的通知需求,比如重大安全隐患、车间紧急疏散这类,可以单独加一个优先级最高的“系统通知”通道,通过 OpenHarmony 的推送服务或本地弹窗实现强提醒,而不是把整个模块的架构搞复杂。
3.2 增量同步接口设计与参数计算
同步接口我定义为 POST /api/notice/sync,请求体如下:
json复制{
"deviceId": "设备唯一标识",
"userId": "当前登录用户 ID",
"maxUpdatedAt": 1735699200000,
"categories": [1, 2, 3, 4],
"page": 1,
"pageSize": 50
}
服务端处理逻辑是:
- 查询
updated_at > maxUpdatedAt且is_deleted = 0的公告,按更新时间倒序排列。 - 如果结果集超过 pageSize,返回第一页数据,并带一个 hasMore 标记,客户端继续循环拉取。
- 同时返回服务端当前的服务器时间 serverTime,客户端将其作为下一次请求的 maxUpdatedAt 基准值。
这里有一个非常重要的细节:为什么用 serverTime 而不是本地时间作为 maxUpdatedAt?因为客户端本地时钟可能不准,如果用户手动改过系统时间,或者时区设置有问题,会导致增量拉取失效或重复拉取。使用服务端返回的时间戳,可以保证对账的基准永远一致。
第一次拉取时,maxUpdatedAt 传 0,服务端会把所有未删除的公告全量返回。这里需要控制单次返回的数据量,我们设置的是每条公告最多返回摘要字段(不包含 content,详情页再单独拉取),这样列表接口的响应体可以控制在几 KB 以内,即使是弱网环境也能较快完成。
3.3 阅读状态上报:批量合并避免频繁请求
阅读状态的上报也是一个容易做坏的地方。如果每读一条公告就调一次接口,技师在公告列表里快速滑动浏览时,会产生大量请求,对服务端造成不必要的压力。
我们做了一个本地上传队列:用户阅读公告时,先在本地把 is_read 置为 1,并写入一条待上报记录(notice_id, user_id, read_time)。同步协议规定,阅读状态存在本地,待网络可用时批量上报。上报接口为 POST /api/notice/read/batch,一次最多上报 50 条记录,服务端处理成功后返回成功状态,客户端清除已上报的记录。
这个设计有一个隐藏的好处:即使用户在离线状态下读了公告,阅读状态也不会丢失,等网络恢复后会自动补交,管理端看到的阅读统计是准确的。对维修管理系统这种强调“安全通知必须确认收到”的场景来说,这个能力相当重要。
3.4 后端存储:为智能通知打基础
既然标题里提到了“智能”,我们给通知公告模块加了一点额外的智能化设计:所有的公告和阅读状态数据,在后端都会落库到业务数据库中,并建立阅读行为分析表。一开始只是简单统计“每条公告被多少人读过”“平均阅读时长多少”,后来我们发现这些数据可以用来做个性化排序——比如某个技师经常查看“发动机维修规范”类公告,那么在公告列表的“推荐”分类里,这类公告的排序权重就会提高。
这个功能需求列表阶段并没有,完全是在开发过程中根据客户反馈迭代出来的。但做好数据落基很重要,如果你在初期就规划好用户行为数据的存储结构,后续做智能推荐、智能提醒都会很省力。
4. Flutter UI 层实现与状态管理
4.1 页面结构与导航设计
通知公告模块的 UI 分为三个主页面:
- 公告列表页(NoticeListPage):顶部是分类 Tab(全部、维修规范、安全通知、排班调整、客户提醒),中间是公告卡片列表,下拉刷新,上拉加载更多,右上角有一个“未读”过滤开关。
- 公告详情页(NoticeDetailPage):展示公告的完整内容,包括富文本渲染、图片、附件下载入口,底部有一个“标记已读”按钮,阅读 3 秒后自动标记已读。
- 消息中心页(MessageCenterPage):与公告列表类似,但展示的是与当前用户相关的个性化消息,消息项右侧显示未读红点。
在导航设计上我们用 go_router 做统一路由管理,公告详情页通过路径参数传递公告 ID,进入详情页时先从本地数据库按 ID 查询,如果本地没有再从接口拉取。这样即使在离线环境下,只要之前浏览过该公告,详情页也能正常打开。
4.2 列表项布局与卡片样式设计
公告卡片的设计我们迭代了很多版本,最终定下来的方案是:
- 左侧为分类标志位,用不同颜色区分维修规范(蓝色)、安全通知(红色)、排班调整(橙色)、客户提醒(绿色)。
- 中间区域为公告标题,优先级高的公告标题加粗并带一个“紧急”标签。
- 右下角显示发布时间,格式为“今天 14:30”“昨天 09:12”“12月20日”这种相对时间,避免显示完整时间戳太占空间。
- 未读公告的卡片背景色略微加深,并在通知图标位置显示红点。
这套设计的核心是信息层级清晰。技师在车间里经常是戴着油手套、快速扫一眼屏幕,他们要的是“一眼看出这条公告重不重要、要不要点进去看”。颜色区分和紧急标签就是最快的视觉线索。
4.3 状态管理:Provider 还是 Riverpod
我们在状态管理方案上最终选择了 Riverpod,主要原因有两点:
第一,Riverpod 对异步状态处理更友好。公告列表有“加载中”“加载成功”“加载失败”“空数据”这几种状态,Riverpod 的 AsyncValue 可以直接映射到页面的 loading、data、error 视图,不需要自己封装状态机。第二,Riverpod 的依赖注入和测试能力更强,我们可以在测试时轻松替换数据库实现,用内存 mock 数据跑 UI 测试。
但这并不意味着 Provider 不行。如果你只是做一个简单的公告列表 + 详情页,没有太多跨页面共享状态的需求,用 Provider 完全够用。我们的消息中心有“未读数量”这个全局状态,它在底部导航栏、列表页、详情页、甚至主界面的角标里都要用到,Riverpod 在管理和监听这种跨模块共享状态时会更方便。
4.4 富文本渲染与附件下载的实现细节
公告内容我们采用 JSON 格式存储,而不是纯 HTML。原因是在 Flutter 中渲染 HTML 需要引入 flutter_html 这类第三方包,体积大且对复杂样式的兼容性一般。我们的富文本结构包含段落、加粗、列表、图片、链接等节点,渲染时用自定义 widget 递归构建。
图片的处理是个重点:公告里的图片如果直接网络加载,在弱网环境下体验会很差。我们的方案是图片本地缓存——服务端上传图片时返回图片 URL 和宽高,客户端渲染详情页时先显示占位符,图片通过 cached_network_image 组件加载并缓存到本地,第二次打开就是秒开。
附件下载功能(比如维修标准 PDF、安全操作手册)的实现没有用插件,而是自己写了一个下载服务,通过 dio 包的 download 方法实现断点下载,进度条显示在底部,下载完成后储存在应用的外部文件目录。在 OpenHarmony 上,附件下载 Toast 提示“已下载至文件管理”即可,不需要引导用户去特定的系统路径找文件。
5. OpenHarmony 平台适配与桥接层实战
5.1 Flutter 环境准备与 OpenHarmony SDK 配置
如果你要在 OpenHarmony 上跑 Flutter,第一步是配置正确的环境。网上关于 Flutter 安装与配置的教程很多,但针对 OpenHarmony 的配置需要注意几个特殊点。
我强烈建议使用 OpenHarmony 社区维护的 Flutter SDK 分支,而不是 Flutter 官方的标准发行版。这个分支的 GitHub 仓库是 gitee 上的 “openharmony-sig/flutter_flutter”,它合并了 OpenHarmony 的平台适配代码。配置时需要注意:
- SDK 分支版本必须与你的 OpenHarmony 系统版本匹配。OpenHarmony 3.2 对应 Flutter 3.7 版本分支,OpenHarmony 4.0 对应 Flutter 3.10 及以上分支。
- 需要同时安装 DevEco Studio(用于构建 OpenHarmony 工程)和 Flutter SDK(用于编写和调试 Dart 代码)。
- 构建 OpenHarmony 的 Flutter 应用时,需要配置 OpenHarmony SDK、toolchain,并在项目根目录添加 flutter 官方维护的 ohos 平台目录结构。
你可能会遇到的第一个坑是:新建 Flutter 项目后,默认只有 android、ios、web 目录,没有 ohos 目录。需要先执行 flutter create --platforms=ohos . 或者从社区模板目录复制 ohos 工程结构。如果执行时提示找不到 ohos 平台,说明你的 Flutter SDK 不是 OpenHarmony 分支,需要重新切换。
5.2 通过 Platform Channel 调用 RelationalStore 的完整步骤
以公告详情页的“标记已读”功能为例,看看 Flutter 层和 OpenHarmony 层是怎么协作的。
Flutter 侧定义一个方法通道:
dart复制class OpenHarmonyDbBridge {
static const platform = MethodChannel('com.example.vehicle_repair/rdb');
static Future<void> markNoticeRead(String noticeId) async {
try {
await platform.invokeMethod('markNoticeRead', {'noticeId': noticeId});
} on PlatformException catch (e) {
debugPrint('markNoticeRead failed: ${e.message}');
}
}
}
OpenHarmony 原生侧,在 MainAbility 的 onCreate 中初始化方法通道:
typescript复制import rdb from '@ohos.data.relationalStore';
import ability_featureAbility from '@ohos.ability.featureAbility';
export default {
onCreate() {
let context = featureAbility.getContext();
let storeConfig = {
name: 'vehicle_repair.db',
securityLevel: rdb.SecurityLevel.S1,
};
rdb.getRdbStore(context, storeConfig, (err, store) => {
if (err) {
console.error('getRdbStore failed: ' + JSON.stringify(err));
return;
}
// 将 store 实例保存为全局变量,供后续查询使用
这个Store = store;
});
}
}
然后注册方法通道的处理器:
typescript复制import rdb from '@ohos.data.relationalStore';
import { BusinessError } from '@ohos.base';
function handleMarkNoticeRead(noticeId: string): Promise<number> {
let predicates = new rdb.RdbPredicates('notice_tb');
predicates.equalTo('id', noticeId);
let values: rdb.ValuesBucket = { 'is_read': 1 };
return 这个Store.update(values, predicates);
}
// 在 MainAbility 中初始化
let methodChannel = new MethodChannel('com.example.vehicle_repair/rdb');
methodChannel.setMethodHandler((method, options) => {
if (method === 'markNoticeRead') {
return handleMarkNoticeRead(options['noticeId']);
}
return Promise.resolve(null);
});
这里有几个要点:
- 数据库名称、表名、字段名要保持和 Android 端 sqflite 一致,这样上层的 Repository 逻辑才能无缝切换。
- 方法通道的参数传递是 JSON 序列化的,注意布尔值在 Dart 和 TypeScript 之间的转换,Flutter 的 true 在原生端拿到的是 true,但如果通过 options 传递 JSON 对象,需要确保类型正确。
- 异步操作要返回 Promise,Flutter 端的 invokeMethod 才能正确 await 到结果。
5.3 通知栏消息与本地通知的实现
通知公告模块除了站内展示,还需要系统通知栏的能力。比如新公告发布时,即使 App 不在前台,也要在状态栏显示一条通知,技师点击通知能直达公告详情页。
Android 端可以用 flutter_local_notifications 插件,iOS 端同理。OpenHarmony 端则需要调用系统的 Notification API。
具体做法是在 OpenHarmony 原生侧封装一个 NotificationHelper:
typescript复制import notification from '@ohos.notificationManager';
import ability_featureAbility from '@ohos.ability.featureAbility';
export function publishNoticeNotification(noticeTitle: string, noticeId: string) {
let request = {
id: noticeId.hashCode(),
content: {
notificationContentType: notification.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
normal: {
title: '新公告',
text: noticeTitle,
},
},
};
notification.publish(request, (err) => {
if (err) {
console.error('publish notification failed: ' + JSON.stringify(err));
}
});
}
需要注意的是,OpenHarmony 的通知服务需要应用自行申请通知权限。在 module.json5 中配置:
json复制{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.NOTIFICATION_CONTROLLER"
}
]
}
}
这里有一个我踩过的坑:某些 OpenHarmony 系统版本上,通知权限只能在应用首次启动时弹窗申请,如果用户拒绝了,后面通过设置页重新开启的路径非常隐蔽。所以在引导流程上,建议在 App 的“设置”-“消息提醒”页面增加一个“开启通知”按钮,调用系统的权限请求接口,而不是只依赖首次启动弹窗。
5.4 关于 flutter 兼容鸿蒙拉起 IAP 支付的插曲
热搜词里有“flutter 兼容鸿蒙拉起 iap 支付”,说明有不少人卡在支付和系统能力接入上。我们的维修管理系统目前没有内购功能,但做过一个配套的“配件商城”模块,涉及支付能力。这里给大家提醒一下:OpenHarmony 的支付能力需要接入华为 IAP 服务,Flutter 侧没有直接的官方插件,需要通过 MethodChannel 调用华为的统一支付 SDK,然后在 OpenHarmony 侧处理支付结果回传。因为涉及证书、签名和商品配置,这部分一般需要单独拉一个分支开发,不要和主流程耦合太深。如果你们的系统里也有支付需求,建议提前摸底开放平台的审核流程,尽量早启动。
5.5 关于 flutter 如何调用鸿蒙图库
另一个在热搜词里的需求是“flutter 如何调用鸿蒙的图库”。公告模块里管理员需要选择图片作为封面或插入正文,我们在 OpenHarmony 上通过 PhotoViewPicker 实现。思路和支付一样:Flutter 侧定义方法通道 pickImages,OpenHarmony 侧调用 photoAccessHelper.PhotoViewPicker.select(),选择完成后返回图片的 uri 列表,Flutter 层通过 uri 加载图片并上传到服务器。注意不同的 OpenHarmony 版本上 PhotoViewPicker 的 API 有差异,4.0 及以上用新的 Picker 接口,3.2 版本则需要降级到旧版的 photoAccessHelper 接口。
6. 常见问题与排查技巧实录
6.1 Flutter 侧依赖包下不下来:SDK 版本不对
很多新人在配置 Flutter 环境时会遇到依赖包下载失败的问题,热搜词里的“flutter 各个版本不对导致依赖包下不下来”说的就是这种情况。我们的经验是:基于 OpenHarmony 分支开发的 Flutter 项目,pubspec.yaml 里最好不要随意指定 SDK 约束太紧的依赖版本,否则 pub 会解析失败。比如你写:
yaml复制environment:
sdk: '>=3.0.0 <4.0.0'
如果本机 Flutter SDK 是 3.7 分支,而某个依赖要求 Dart 3.2 以上,就会导致解析失败。建议把 SDK 约束放宽到 <4.0.0,依赖版本也不要锁定精确版本,用 caret 范围。实在不行就删掉 pubspec.lock,执行 flutter pub get --no-precompile 重新拉取。
6.2 OpenHarmony 设备上数据库创建失败或表不存在
在 OpenHarmony 真机或模拟器上联调时,最常遇到的问题是数据库操作报 “table not found” 或 “database is locked”。
排查步骤一般是:
- 确认你在初始化 RelationalStore 时使用了正确的数据库名称和路径,不要和别的模块共用同一个 store 实例导致表冲突。
- 确认建表 SQL 在主工程的 databaseHelper 中执行过,并且建表操作是幂等的(CREATE TABLE IF NOT EXISTS)。
- Android 端用 sqflite,OpenHarmony 用 RelationalStore,两边建表 SQL 要严格一致,包括字段名、字段类型、默认值,否则同一套 Repository 逻辑在两端可能出现时区不一致、布尔值类型不一致等问题。
- 如果出现 “database is locked”,多半是因为某个长事务没有关闭。检查任何没有自动 commit 的事务,确保 update/insert 操作后有正确的回调或 await 返回。
6.3 UI 层 CheckboxListTile 文字距离按钮太近
flutter 开发中一个看起来很“小”的 UI 问题,实际很影响使用体验:CheckboxListTile 的标题文字和复选框之间的距离太近,导致视觉上挤在一起。这个问题在公告筛选页面(比如多选分类)尤其明显。
解决方案其实很简单,调整 CheckboxListTile 的 contentPadding 和 title 的 padding。或者在列内部使用 Row + Checkbox + Text 的组合,而不是直接使用 CheckboxListTile。我个人更推荐后者,因为完全可控,不受 Material 版本的影响。
dart复制Row(
children: [
Checkbox(
value: isChecked,
onChanged: (value) => setState(() => isChecked = value!),
),
const SizedBox(width: 12),
Expanded(child: Text(title, style: const TextStyle(fontSize: 14))),
],
)
6.4 Flutter Web 上字体变小的问题
我们的管理系统还有个 Web 管理端,标题中的“通知公告模块”也要在 PC 浏览器上展示。有段时间测试反馈说 Web 端公告标题字体特别小,比手机端还小。排查后发现是 html renderer 对媒体查询的默认字号处理与 cupertino 不同导致的。
解决方案是在 main() 里统一设置一个 baseline 字体:
dart复制void main() {
runApp(const App());
}
并在 MaterialApp 的 theme 中显式设置 TextTheme 的各类字号,不要依赖系统默认。Web 端 build 时加上 --web-renderer html 参数,也能规避部分兼容问题。不过现在的 Flutter Web 版本已经比较成熟,这个问题在新版本里出现概率会低一些。
6.5 反编译 Flutter 应用的加固经验
既然有车辆维修管理系统这种行业应用,最后说一个很多人没意识到的问题:反编译。Flutter 应用的 Dart 代码被打包进 so(Android)或 HAP(OpenHarmony)里,但 Dill 字节码是可以被还原的。之前有同行分享过某个 Flutter 应用被反编译、核心业务逻辑泄露的案例。
在项目上线前,我们采用了几道加固措施:业务中最核心的签名校验、鉴权 token 存储放在原生端,不写在 Dart 里;Dart 代码编译时开启混淆(flutter build 的 release 模式默认会混淆变量名,但只能做到混淆,不能做到完全不逆转);涉及薪酬、客户隐私的数据请求走加密通道,不上明文日志。别让别人的“反编译 flutter”搜索词里有你 App 的影子。
7. 最后的几个实操心得
把这套 Flutter × OpenHarmony 跨端车辆维修管理系统的通知公告模块完整走通之后,我最大的感受有几条,分享给准备上手类似项目的朋友。
第一,不要把 OpenHarmony 当成 Android 的“另一种皮肤”。它有自己的系统 API、自己的生命周期、自己的并发模型。Flutter 的跨端优势是 UI 和业务逻辑层,但一旦涉及系统能力(数据库、通知、文件、相册、推送),你必须有做平台适配的心理准备。把桥接层当作一个独立的工程来对待,而不是“有空再补的胶水代码”,否则后面的坑会越来越多。
第二,离线优先不是一句空话,它体现在每一个数据操作的设计里。本地数据库不是缓存的替代品,而是和远程数据库“平级”的存在。你在设计表结构、同步协议、冲突处理策略时都要从“本地数据一定是可靠的”这个前提出发。这样才能在弱网、断网环境下给维修车间的技师提供不卡壳的公告查阅体验。
第三,设备碎片化比想象中严重。RK3568 只是 OpenHarmony 众多芯片平台之一,还有 RK3588、Hi3751、Hi3516、麒麟系列等。每一款芯片的 GPU 驱动、视频编解码能力、系统版本都有差异,Flutter 应用在不同的设备上表现可能完全不同。建议建立自己的真机测试矩阵,至少覆盖主力设备 3 款以上,在开发阶段就暴露性能问题,而不是等现场部署后才被动响应。
通知公告模块只是这套维修管理系统的一个切片,但它把 Flutter 跨端开发、OpenHarmony 平台适配、本地数据库+后端同步、UI 状态管理这些关键技术点全部串了起来。把这套思路吃透,再做消息中心、工单提醒、设备预警这些模块时,你会发现架构是现成的,剩下的就是往里面填业务逻辑。做跨端项目就是这样,前面搭好骨架,后面就能跑得又快又稳。
