最近在推进一个用 Flutter 开发的生活助手 App,目标设备是搭载 OpenHarmony 的国产板子和手机。需求里头最让我花时间的不是记账本身,而是"收入分析统计"这一块。说实话,Flutter 跑在 OpenHarmony 上已经不是新鲜事,社区分支越来越稳,但真要落地一个涉及数据存储、聚合计算、图表绘制的模块,坑比想象中多。这篇就把我实现收入分析统计的完整过程写下来,包括需求拆解、数据层选型、统计口径设计、图表适配、平台通道桥接,以及最后在真机上踩过的那些版本和渲染问题。想用 Flutter 做 OpenHarmony 应用、或者正在写数据分析模块的朋友,可以直接参考这里面的思路和代码。
1. 先理清需求:这个收入分析模块到底要做什么
很多新手拿到"收入分析统计"第一反应就是画几个图表。但需求方嘴上说的"简单统计一下",落地时往往是另一套复杂度。我先把原始需求拆成了三层:记录、聚合、展示。记录层负责把每一笔收入数据写进本地数据库;聚合层按日、周、月、年做分类汇总;展示层把结果渲染成折线图、柱状图、饼图,并且支持用户切换时间范围。
1.1 从"记账"到"分析":用户的真实使用场景
生活助手 App 的收入分析,用户大概率不是专业会计,而是想知道"我这个月一共赚了多少""哪类收入占大头""上个月和这个月比是涨还是跌"。所以这个模块不只是一个表单,它背后要支撑三个高频操作:
- 用户每天或每周手动录入一笔收入:工资、兼职、理财、红包、二手卖出等;
- 用户希望首页直接看到本月总收入、环比上月的变化百分比;
- 用户点进详情页,能看到分类占比、每日趋势、单笔记录明细。
我的切入点是把"收入"当成一个独立实体,而不是跟支出混在一起。这样统计口径会清晰很多。如果你之前做过账单类 App,一定知道收支混在一个表里后面聚合时别提多难受。所以第一步,我把收入表单独建。
1.2 收入类型与"是否固定"的标记
经过跟需求方沟通,收入要支持三类:固定收入、浮动收入、一次性收入。
- 固定收入:比如工资,每月同一天到账,金额基本不变;
- 浮动收入:比如按业绩提成、股票分红,金额不固定但有周期性;
- 一次性收入:比如红包、奖金、二手转卖,发生时间随机。
这套分类不是拍脑袋,它直接影响后面的"同比环比"计算。比如浮动收入按月波动很大,如果不做标记,统计趋势时会把整体均值带偏。我后来在设计表结构时加了一个income_type字段,同时加了一个is_recurring布尔值,用来区分是否支持周期预测。
1.3 统计口径:读懂"月度汇总"和"分类占比"的不同算法
统计口径是最容易出 bug 的地方。月度汇总有两种做法:按"收入发生日期"汇总,和按"到账日期"汇总。生活助手场景下,用户录入的多笔收入可能发生在月初但到账在月末。经过权衡,我最终统一按"发生日期"(occurred_at)做统计,因为用户认知里"这笔钱是哪天赚的"比"哪天到账"更直观。
分类占比则是用category_id分组,计算每种分类金额占总金额的百分比。要注意的是,当用户选择时间范围时,分类占比和月度汇总必须使用同一个过滤条件,否则图表之间会打架。我后面的实现里,把筛选条件封装成了一个IncomeQuery对象,统一传给数据库层和图表层,保证口径一致。
2. Flutter 接入 OpenHarmony 的工程准备与版本踩坑
做 OpenHarmony 的 Flutter 开发,第一关不是业务代码,而是环境。OpenHarmony 官方并没有把 Flutter 作为首选应用框架,但是社区分支flutter_flutter一直在同步上游版本,目前已经可以在 OpenHarmony 3.2/4.0 左右的设备上稳定跑起来。搭建环境时我踩了不少坑,尤其是版本匹配问题。
2.1 开发环境:DevEco Studio 与 Flutter SDK 的搭配
我的开发机装了 DevEco Studio 作为 OpenHarmony 原生工程的管理工具,同时安装了社区维护的 Flutter SDK 分支。这个分支本质上是在某个 Flutter 稳定版本上打了一个ohos平台的补丁,所以你要注意分支对应的版本号,不能随便拿一个 Flutter 稳定版就用。
我这边最终选用的版本组合是:Flutter 3.19 分支的 ohos 适配版,搭配 OpenHarmony 4.0 Release SDK。如果你用 3.22 或者更新的分支,反正一定要看flutter doctor是否认出了ohos这个平台。认不出来就检查PATH环境变量和 Flutter SDK 目录下的bin是否指向正确分支。
2.2 创建 ohos 平台的 Flutter 工程:三端目录结构
用命令行创建工程后,目录里会多出一个ohos文件夹,跟android、ios同级。这个ohos目录下是完整的 OpenHarmony 工程结构,包括entry/src/main里的ets原生代码和module.json5配置文件。
我第一次创建工程时以为还需要手动用 DevEco 建一个空工程再把 Flutter 模块嵌进去,结果完全不用。flutter create --platforms=ohos .一下就能生成可用的工程骨架。要注意的是,如果你是在已存在的原生 OpenHarmony 工程里集成 Flutter,那要走另一套"原生工程嵌入 Flutter 模块"的流程,跟纯 Flutter 工程不一样。这个后面在平台通道部分我会再提。
2.3 版本适配警告:the current configured flutter sdk is not known to be fully supported
这是我最想提醒大家的坑。跑flutter doctor或者打开 IDE 时,经常会看到一句提示:the current configured flutter sdk is not known to be fully supported. please check your installation.这个提示不代表一定跑不起来,但表示当前 Flutter SDK 版本跟项目配置文件里的minSdkVersion或者 ohos 工具链版本组合没被官方回归测试覆盖过。
我遇到的情况是:升级了 OpenHarmony SDK 到 4.1,但 Flutter 分支还停留在 3.19,结果编译时一堆 ndk 和 toolchain 的警告。后来我重新检查了ohos工程的build-profile.json5,把compileSdkVersion和compatibleSdkVersion调到 Flutter 分支支持的范围,警告才消失。
我的建议是:不要一见到警告就忽略,也先别急着追最新版。记录下当前 Flutter 分支支持的 OpenHarmony SDK 版本范围,在pubspec.yaml和build-profile.json5里保持一致,这样后续调试能省很多事。
3. 数据层实现:SQLite 在 OpenHarmony 上的可用方案
收入数据必须持久化,而且大部分统计操作都依赖聚合查询。我第一个想到的是sqflite,这是 Flutter 社区最常用的 SQLite 插件。但 OpenHarmony 不是 Android,插件能不能直接用,需要实际跑一下。
3.1 为什么选 sqflite 而不是其他数据库方案
我先梳理了可选方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| sqflite | 接口成熟,社区例子多 | 原版插件未适配 ohos,需要找 ohos 兼容分支 |
| drift | 类型安全,查询编译器 | 底层还是依赖 sqlite,加入额外编译层 |
| hive | 纯 Dart,无原生依赖 | 复杂聚合查询不够方便 |
| ObjectBox | 性能强 | 初步适配成本高,设备兼容性未知 |
我最后用的是一个 OpenHarmony 社区维护的sqflite_fork包。原因是收入分析统计需要大量GROUP BY、SUM、BETWEEN查询,SQL 写起来最直接。hive 那种 key-value 想做按月聚合,得把全量数据拿到内存里算,数据量一大就吃力。drift 固然好,但它在 ohos 上需要额外配置 native 库,我不想在一开始引入太多变量。
3.2 收入表设计与迁移
收入表我设计成这样的核心字段:
class IncomeRecord { final int? id; final double amount; final int categoryId; final DateTime occurredAt; final String note; final String incomeType; // fixed | variable | one_time final bool isRecurring; }对应的建表 SQL 是这样:
CREATE TABLE income_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, amount REAL NOT NULL, category_id INTEGER NOT NULL, occurred_at INTEGER NOT NULL, note TEXT, income_type TEXT NOT NULL DEFAULT 'one_time', is_recurring INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL );金额我用REAL而不是INTEGER,因为收入可能带小数。时间存储我用毫秒时间戳,避免 SQLite 里日期格式的乱七八糟问题。建表语句里我还加了index_income_occurred_at索引,这个很重要,否则按月查询时全表扫一遍,几百条数据还行,上万条就明显卡顿。
迁移我放在onCreate和onUpgrade里管理。因为这是第一个版本,我从一开始就把version设置为 1,后续加字段就直接递增版本号,在onUpgrade里做ALTER TABLE。
3.3 沙箱路径与数据文件管理:OpenHarmony 权限模型
在 Android 上,sqflite 默认数据库路径是getDatabasesPath()返回的应用私有目录。OpenHarmony 也类似,但路径细节不同。我跑起来发现,用原版getDatabasesPath()在 ohos 上返回的路径可能不存在,需要先确认目录是否存在,否则 sqlite 打开数据库会失败。
我最后在启动时加了一段目录初始化逻辑:
final dir = await getDatabasesPath(); final dbPath = '$dir/income.db'; if (!await Directory(dir).exists()) { await Directory(dir).create(recursive: true); }还有一个容易忽略的点:OpenHarmony 对应用沙箱有严格的访问控制,不像 Android 那样能随便读写公共存储。收入数据是敏感的个人财务数据,我也没必要放到公共目录,直接用应用私有目录最稳妥。但这里我提醒一下:如果你要把数据库导出到用户可见的文档目录,得申请对应的权限,这个跟 Android 的运行时权限完全不同,OpenHarmony 用的是ohos.permission.WRITE_MEDIA之类,需要提前在module.json5里声明。
4. 收入分析的计算逻辑:用 Dart 把统计写清楚
接下来是核心逻辑部分。统计不能每次拿到全量数据在内存里算了再塞给图表,应该在 SQL 层先把聚合处理好,Dart 层只负责把结果转换成图表需要的结构。这样既简洁又高效。
4.1 月度汇总与分类占比:SQL 里的 GROUP BY
我写了一个IncomeStatisticsRepository,核心方法用 SQL 直接算月度汇总:
SELECT strftime('%Y-%m', occurred_at / 1000, 'unixepoch') AS month, SUM(amount) AS total FROM income_records WHERE occurred_at BETWEEN ? AND ? GROUP BY month ORDER BY month ASC;这里有个细节:SQLite 的strftime默认处理的是秒级时间戳,如果我把时间存成毫秒,必须先除以1000。我第一次没除,导致月份全部错乱,最后排查了好久才发现是时间戳单位的问题。
分类占比更简单:
SELECT category_id, SUM(amount) AS amount, COUNT(*) AS count FROM income_records WHERE occurred_at BETWEEN ? AND ? GROUP BY category_id ORDER BY amount DESC;拿到category_id后,在 Dart 里映射成中文分类名称。分类名称我单独建了一张income_categories表,避免在代码里写死字符串。用户后续自定义分类时,只需要改表数据,不用改代码。
4.2 趋势环比与同比增长:先算出上一周期的基准值
"环比上月"是收入分析的高频词。它的计算方法是:本月总金额减去上月总金额,除以上月总金额,得到百分比。
Future<double> monthOverMonth(DateTime currentMonth) async { final current = await totalForMonth(currentMonth); final previousMonth = DateTime(currentMonth.year, currentMonth.month - 1); final previous = await totalForMonth(previousMonth); if (previous == 0) return 0; return (current - previous) / previous * 100; }边界情况要处理:如果上月收入为 0,计算会除零。我的策略是返回 0,并在界面上显示"暂无对比数据"。同比也是同理,只是往前推一年。在实际场景里,固定收入用户其实最关心环比,因为工资变动是大事;而兼职用户更关心月度之间的波动。
4.3 自定义时间段的筛选实现
首页默认展示本月,但用户可能会点一个日历控件,选择"3月1日到3月20日"。我的做法是把起止时间转成毫秒时间戳,再传给 SQL 的BETWEEN条件。这里需要注意时区问题:
DateTime.now()拿到的是本地时区;- SQLite 存的是本地时区的毫秒时间戳;
- 筛选时也必须用本地时区的时间戳。
如果你在代码里用了DateTime.utc或者toUtc(),时间一转换就会差 8 个小时,月度统计在月初月末极容易串月。我干脆统一约定:所有时间戳都按本地时区存储,不存 UTC。这对单机应用来说最简单,用户换时区的场景不在考虑范围。
5. 图表可视化的选型与 OpenHarmony 适配
收入分析最终要落到图表上。Flutter 里常见的图表库我大部分都试过,但到了 OpenHarmony 平台,有些库会直接罢工。
5.1 fl_chart 与 CustomPaint 的取舍
我一开始用fl_chart,它功能全,柱状图、折线图、饼图都有现成组件。在 Android 上运行良好,但拿到 OpenHarmony 真机上一跑,发现饼图的绘制出现奇怪的偏移,折线的动画也会卡顿。排查后怀疑是fl_chart内部用了大量Canvas绘制,而 OpenHarmony 的 Flutter 引擎对某些Paint效果支持还不完整。
我没死磕第三方库,直接退一步:如果只需要展示月度趋势、分类占比、每日柱状图,完全可以用CustomPaint自己画。工作量不大,而且能精确控制绘制逻辑,避免第三方库在 ohos 上的兼容坑。
5.2 用 CustomPaint 画柱状图和折线图的实现思路
柱状图的核心是计算每个柱子的高度比例。我先把数据归一化到 0~1,再映射到画布高度:
final maxValue = data.values.reduce((a, b) => a > b ? a : b); final barWidth = size.width / data.length * 0.6; for (var i = 0; i < data.length; i++) { final barHeight = data.values[i] / maxValue * maxBarHeight; canvas.drawRect( Rect.fromLTWH( startX + i * spacing, size.height - barHeight, barWidth, barHeight, ), paint, ); }折线图则需要把每天的收入数值转成偏移点,再用Path连接起来。这里有个小技巧:画折线之前先调用canvas.drawPath绘制渐变填充区域,视觉上更美观,代码也就多几行。
自定义绘制的好处是无论 Flutter 引擎怎么升级,只要 Canvas API 稳定,图表就不会崩。代价是要自己处理坐标轴、网格线、文字标注,但这些都是体力活。
5.3 部分组件在 ohos 上无效的替代方案
实测中我发现,fl_chart里的某些交互手势(比如拖动缩放)在 OpenHarmony 上响应不灵敏,因为底层平台手势事件映射有所差异。如果一定要保留交互,我建议在图表外层包一层GestureDetector,自己通过onHorizontalDragUpdate去维护当前选中的数据索引,然后重绘。这个逻辑不复杂,我也已经写进代码里了。
此外,文字渲染在部分 OpenHarmony 设备上默认字体对中文支持不友好,尤其是数字和中文混排。我的方案是在图表区域强制指定一个系统字体族,比如fontFamily: 'sans-serif',中文字体则用系统自带的中文字体。如果你在画图上发现文字变成了方块,优先检查字体设置。
6. 平台通道与原生能力桥接:MethodChannel 和 EventChannel 的实际用途
Flutter 在 OpenHarmony 上跑得再顺,总有一些能力是 Flutter 层拿不到的。比如读取系统日历、访问相册、监听系统网络状态、获取设备电量。收入分析模块里,有几个数据来源必须通过平台通道从原生层取。
6.1 为什么部分收入数据必须从原生侧拿
理想状态下,用户手动录入每一笔收入就够了,但需求方想加一个"自动导入"功能:从系统短信或支付通知里解析收入。OpenHarmony 的短信数据在应用沙箱外,Flutter 层根本没有权限直接读,必须通过原生层调用系统 API,然后把结果返回给 Dart。
这个场景如果硬要绕过原生,用外接辅助功能去监听,权限和合规问题都很麻烦。我的做法很直接:用MethodChannel调用原生代码,回调一个 JSON 数组,里面是解析出来的候选收入记录。
6.2 MethodChannel 调用的正确姿势
Flutter 侧定义通道名和调用方法:
static const platform = MethodChannel('com.example.lifeassist/income'); final result = await platform.invokeMethod('getIncomeCandidates');OpenHarmony 原生侧(ets 代码)里用MethodChannel注册同名通道。因为我主要写 Dart,ets 这部分代码只写了一小段,大致逻辑是:
let methodChannel = new MethodChannel('com.example.lifeassist/income'); methodChannel.setMethodCallHandler((call) => { if (call.method === 'getIncomeCandidates') { // 调用系统 API 获取短信或通知记录 } });这里要特别注意:通道名必须完全一致,任何大小写差异都会导致MissingPluginException。我在调试时遇到过几次通道调用失败,结果都是因为原生工程里没有正确注册插件注册表,导致 Flutter 侧发出的调用找不到原生 handler。
6.3 EventChannel 实时接收汇率或账户变动
收入统计如果涉及外币,需要实时汇率。汇率是外部数据,我可以用EventChannel让原生层不断推送最新汇率,Dart 侧用流去监听。不过实际开发中,我更倾向于用 Dart 的http包直接请求网络接口,没必要通过原生层绕一圈。只有像"系统日历中的工资日提醒"这类系统事件,才适合用EventChannel订阅。
我在这个版本里用EventChannel做了一件事:当 OpenHarmony 系统时间变化时,通知 Flutter 侧重新刷新当天的收入统计。场景是用户跨时区或者手动改时间后,图表上的"今天"必须立刻更新。原生侧监听系统时间改变事件,然后通过EventChannel的success方法把时间戳推给 Dart。
6.4 PlatformView 嵌入原生图表组件的尝试
有些开发者会想,是不是可以在 Flutter 里嵌入一个 OpenHarmony 的原生统计图表组件,比如直接用 ArkUI 的Charts。我试过PlatformView,能跑通,但有两个问题:
- Flutter 和原生视图之间的层级协调在滚动时会出现卡顿;
- 原生图表的触摸事件和 Flutter 手势手势识别器有冲突。
最终我放弃了 PlatformView,把所有图表都放到 Flutter 层用CustomPaint实现。我的经验是:除非原生组件特别复杂且不可替代,否则不要轻易在 OpenHarmony 上用 PlatformView,性能损耗和事件冲突的调试成本会拖垮整个项目。
7. 性能优化与发布前的那些检查
收入分析模块虽然数据量不会特别大,但在低端 OpenHarmony 设备上,图表和数据库查询依然可能卡顿。发布前我做了几项优化,效果立竿见影。
7.1 首帧耗时与图表绘制性能优化
Flutter 在 OpenHarmony 上首帧加载速度明显比 Android 慢,尤其是从原生页跳转到 Flutter 页面时。我发现主要原因是初始化时加载了太多全局插件。我的做法是:
- 延迟加载非必要插件:比如
MethodChannel通道不要写在main()里初始化,而是在第一次调用时才注册; - 图表页面延迟加载:收入详情页用
FutureBuilder先显示一个加载骨架屏,等数据库聚合完成后再绘制图表; - 使用
RepaintBoundary隔离每个图表的绘制区域,防止某个图表重绘时牵连整个页面。
另外,如果图表区域在滚动页面里,记得把isComplex设为 true,willChange设为 false,合理利用光栅缓存能有效提高滚动流畅度。
7.2 ABI 与安装包体积控制
OpenHarmony 设备有多种 CPU 架构,默认编译会打出包含所有 ABI 的 hap 包,体积会比较大。我使用abiFilters只保留实际设备的架构。比如 RK3568 板子是 arm64-v8a,那就只保留这一个。这样 hap 包体积能减少 30% 左右。
还有一个更粗暴但有效的方案:用--split-debug-info和--obfuscate做混淆,同时移除不必要的clear-text traffic权限配置。因为收入数据是隐私敏感数据,网络请求必须走 HTTPS,明文流量直接禁用。
7.3 OpenHarmony 签名与 hap 打包注意事项
发布到 OpenHarmony 应用市场时,应用包必须用发布证书签名,否则无法安装。这里需要走 DevEco Studio 的签名管理流程,在build-profile.json5里配置好signingConfigs。
我踩过一个大坑:本地调试时用 debug 签名运行没问题,但用 release 签名打包后,数据库路径变了,导致用户升级安装后原来的收入数据"消失"了。原因是不同签名下可能使用不同的沙箱隔离区。后来我在代码里加了一道数据迁移逻辑:启动时检测到新路径下没有数据库,就尝试把旧路径下的数据库文件复制过来。这个问题暴露了测试不充分的后果,建议大家从第一个版本开始就在真机上用 release 签名做升级测试。
8. 实测结果与几点经验体会
收入分析模块在整合完成后,我在几台 OpenHarmony 设备上做了验证,包括 RK3568 开发板、MatePad 的 OpenHarmony 版本、还有一台用的低配手机。结果整理成表格如下:
| 设备 | CPU/内存 | 页面对启动耗时 | 图表绘制帧率 | 备注 |
|---|---|---|---|---|
| RK3568 板子 | 四核 A55 / 4GB | 1.8s | 45fps | 部分动画偶尔掉帧 |
| 中端平板 | 八核 / 6GB | 1.2s | 58fps | 流畅,无明显卡顿 |
| 低配手机 | 六核 / 4GB | 2.0s | 40fps | 图表切换时有掉帧 |
整体来看,Flutter 在 OpenHarmony 上的表现已经可以支撑业务落地,但离 Android 的流畅度还有差距。低端设备上,图表绘制和页面切换如果能减少动画,体感会好很多。
8.1 我最想分享的三个避坑经验
按痛苦程度排序:
- 时间戳单位问题。SQLite 里
strftime用的是秒级,但我存了毫秒,导致月度统计结果完全错乱。这个 bug 排查了整整一个下午。 - 通道注册时机。
MethodChannel如果放在 Dart 侧main()里就直接调用,而原生侧插件注册表还没来得及初始化,就会报MissingPluginException。稳妥的做法是等第一个页面渲染完成后再调用原生通道。 - 发布证书与沙箱路径。不同签名的应用可能对应不同的沙箱目录,升级安装后数据库文件可能不迁移。这个问题覆盖面很广,一定要在正式发布前写好迁移逻辑。
8.2 这个模块后续还能怎么扩展
收入分析目前还只是基于本地录入数据和系统解析数据。后续可以加一个云同步功能,把脱敏后的统计数据传给用户自己的账户,实现多设备之间的历史对比。或者引入更细粒度的异常检测,比如某个月收入骤降 50% 时,在首页给一个提醒卡片。
我个人的看法是,Flutter for OpenHarmony 的生态还在快速迭代,作为开发者,不要等所有插件都稳定了再动手。先把核心链路用最朴素的方式跑通,比如自己写 SQL、自己画图表,最后再逐步替换成更完善的三方库,这条路在 OpenHarmony 上依然走得通。
最后分享一个小技巧:开发阶段不要只在模拟器上验证。用真机连着 DevEco Studio 跑flutter run --device-id ohos,能看到最真实的渲染效果和日志输出。收入统计这种对图表精度有要求的模块,模拟器和真机的差距会非常大,早发现早解决,省下的时间绝对值得。