ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter OpenHarmony收入分析统计实战:从SQLite到CustomPaint

Flutter OpenHarmony收入分析统计实战:从SQLite到CustomPaint 最近在推进一个用 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 --platformsohos .一下就能生成可用的工程骨架。要注意的是如果你是在已存在的原生 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 趋势环比与同比增长先算出上一周期的基准值环比上月是收入分析的高频词。它的计算方法是本月总金额减去上月总金额除以上月总金额得到百分比。Futuredouble 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注册同名通道。因为我主要写 Dartets 这部分代码只写了一小段大致逻辑是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设为 truewillChange设为 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 / 4GB1.8s45fps部分动画偶尔掉帧中端平板八核 / 6GB1.2s58fps流畅无明显卡顿低配手机六核 / 4GB2.0s40fps图表切换时有掉帧整体来看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能看到最真实的渲染效果和日志输出。收入统计这种对图表精度有要求的模块模拟器和真机的差距会非常大早发现早解决省下的时间绝对值得。
返回列表