我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Flutter for OpenHarmony实战:理财App分类分析页面开发全解析

Flutter for OpenHarmony实战:理财App分类分析页面开发全解析 年前那会儿公司在评估金融类App的跨端方案正好赶上OpenHarmony生态起来手头又接了一个个人理财管理的练手项目最后敲定用Flutter for OpenHarmony来做。整个开发过程挺有意思的尤其是分类分析页面这一块踩了不少坑也积累了一些心得。这篇文章就围绕这个分类分析页面的实现把技术选型、数据模型、图表渲染、交互联动这些环节完整拆一遍包括遇到的问题和对应的解决方案给打算在OpenHarmony上跑Flutter的朋友一个参考。1. 项目背景与技术选型思路1.1 为什么选择Flutter for OpenHarmony先说结论如果你团队已经有Flutter基础想在OpenHarmony设备上快速出应用Flutter for OpenHarmony是目前性价比最高的路线。这一点在分类分析这种数据密集型页面上体现得尤其明显。OpenHarmony本身主推的是ArkUI声明式开发如果只做单端ArkUI完全够用。但现实情况是绝大多数开发团队手里已经有成熟的Flutter代码库或者大量Flutter开发经验重新用ArkUI写一套成本是翻倍的。Flutter for OpenHarmony的价值就在于让一套Dart代码可以跑在Android、iOS、OpenHarmony多个平台上UI逻辑基本不用重写。我在做这个项目前对比过两条路线纯ArkUI开发需要学声明式UI、状态管理、路由体系团队学习成本高而且后续如果要适配Android端等于再写一遍。Flutter for OpenHarmony沿用Flutter的Widget树、状态管理、渲染管线主要工作集中在设备适配和插件替换上。实际测试下来Flutter for OpenHarmony在图形渲染层的性能表现比预期好。分类分析页面涉及饼图、柱状图、趋势折线图三种图表帧率稳定在55到60帧之间跟原生绘制的差距在可接受范围内。1.2 技术栈整体规划分类分析页面不是孤立的一个页面它是整个理财App里负责数据可视化和消费洞察的核心模块。整个项目的技术栈如下UI框架Flutter 3.7.12OpenHarmony适配版本开发工具DevEco Studio 4.0配置OpenHarmony SDK状态管理Provider ChangeNotifier图表库fl_chart定制开发数据存储hive本地轻量级数据库数据来源本地模拟数据手动记账数据选择fl_chart而不是自带绘制的SelfPainter是因为fl_chart提供了现成的扇形图、柱状图API而且它的绘制是基于Canvas的在Flutter for OpenHarmony上可以直接工作不需要额外的原生桥接。这一点很重要因为OpenHarmony的插件生态远不如Android/iOS成熟很多Flutter插件在OpenHarmony上要么需要自己适配要么干脆不支持。像fl_chart这种纯Dart实现的库几乎是零成本迁移。2. 分类分析页面的核心需求与数据模型设计2.1 页面功能需求拆解分类分析页面在个人理财App里的定位是帮助用户回答这么几个问题我的钱都花到哪里去了哪些类目是支出大头这个月跟过去几个月相比消费结构发生了什么变化我的预算执行得怎么样哪个类别快超额了围绕这些问题页面被拆成四个功能模块分类占比环形图展示当月各类支出的金额占比分类支出Top5列表带进度条和预算超支状态近6个月支出趋势折线图支持按分类维度切换分类筛选器可以在饼图和趋势图之间做联动2.2 数据模型设计数据模型是整个页面的地基。要是模型设计得不好后面做图表渲染和联动切换的时候会非常痛苦。我第一版在数据模型上偷了懒直接用Map结构传数据结果写联动逻辑的时候到处都是类型判断和空值兜底。后来重构成了明确的实体类整个页面的数据流转一下就清晰了。核心实体有三个// 账单记录 class BillRecord { final String id; final String categoryId; // 分类ID final double amount; // 金额 final DateTime date; // 交易日期 final String remark; // 备注 } // 分类信息 class Category { final String id; final String name; // 分类名称如餐饮、交通 final String icon; // 图标标识 final Color color; // 图表中的颜色 final double budget; // 月度预算 } // 分类聚合数据 class CategoryAggregate { final Category category; final double totalAmount; // 当前时间范围内的支出总额 final double percentage; // 占总支出的比例 }这里有个细节值得说一下为什么饼图的数据源不用地图直接用聚合数据因为fl_chart的PieChartSectionData需要amount、title、color这几个字段直接用CategoryAggregate可以避免在build方法里做无意义的字段抽取代码可读性和维护性都会好很多。2.3 数据聚合的算法实现分类分析的数据聚合逻辑是按月分组按分类汇总。这个逻辑看起来简单但真正实现的时候有几个容易踩坑的点。首先是时间范围的边界问题。当月的数据范围应该是当月1号00:00:00到下个月1号00:00:00而不是当月31号23:59:59因为每个月的天数不一样直接用DateTime(now.year, now.month 1, 1)这种方式生成下月边界是最稳妥的。其次是金额的精度问题。涉及金额计算直接用double会有精度丢失风险。我在第一版就遇到过0.1 0.2不等于0.3的问题导致饼图占比计算出现偏差。后来统一改用int存储分只在展示层转为元。这个转换逻辑放在模型里做一个计算属性class BillRecord { final int amountInCents; // 以分为单位存储 double get amountInYuan amountInCents / 100; }第三是分类汇总后的排序。饼图展示和Top5列表的排序逻辑是一致的都按支出金额降序排列。在聚合计算完成后用sort方法按totalAmount降序排列然后分别喂给图表和列表组件。3. 页面整体布局与视觉方案3.1 页面结构设计分类分析页面的整体布局从上到下分为筛选区、统计卡片、环形图、Top5列表、趋势图。这个结构是标准的数据分析页黄金布局核心原则是先让用户看到结论再让用户看细节。顶部是一个横向滚动的分类筛选器默认选中全部点击某个分类后下方的环形图和趋势图会联动刷新。这个筛选器的设计参考了主流理财App的交互模式但做了一点简化去掉了搜索结果联动的复杂度只保留单一维度的筛选。中间的环形图区域和统计卡片区域放在一个Card容器里。Card带圆角和阴影在整体白色背景下形成层次感。统计卡片显示的是当月总支出和预算结余两个核心指标用大号字体突出显示。3.2 图表配色与主题适配配色方案直接影响数据可视化效果。分类分析页面涉及多个图表颜色需要保证在饼图上能区分、在趋势图上能突出重点、在列表里能形成统一视觉感知。我在项目里定义了一套统一的分类色板餐饮橙红色 #FF6B35交通蓝色 #4E8FFF购物紫色 #A16AE8居住青色 #00B4A8娱乐粉红色 #FF6B9C医疗绿色 #2ED573教育黄色 #FFA502其他灰色 #8E8E93这套颜色在设计时遵循了两个原则同类色相区分明显色相之间不混淆在浅色背景和深色文字上都有足够的对比度。实际操作中我测试过在深色模式下的效果发现部分浅色如黄色在深色背景上辨识度不够所以在didChangeDependencies里根据Theme.brightness做了色值微调Color resolveCategoryColor(Category category, Brightness brightness) { if (brightness Brightness.dark) { return brightnessAdjustedColor(category.color); } return category.color; }这个细节虽然小但在用户切换主题模式后图表的阅读体验会完全不同。3.3 适配不同屏幕尺寸OpenHarmony设备覆盖了手机、平板、开发板等多种形态。分类分析页面在设计之初就考虑了自适应布局。实现方式并不复杂核心是两套逻辑宽度小于400dp的设备筛选器和图表保持单列布局统计卡片横向放两个宽度大于600dp的设备平板整体切换为双栏布局左侧是饼图和筛选器右侧是趋势图和Top5列表具体通过MediaQuery.of(context).size.width来判断而不是用断点区间因为代码里只需要两种情况切换没必要引入复杂的栅格系统。这种少即是多的适配策略在真实项目中比引入重型布局框架更实用。4. 核心功能实现与关键代码解析4.1 环形图的实现环形图是分类分析页面的核心视觉组件它展示的是各个消费分类的占比关系。fl_chart的PieChart可以很方便地实现这个效果。PieChart( PieChartData( sections: _buildPieSections(aggregates), centerSpaceRadius: 48, startDegreeOffset: -90, sectionsSpace: 2, pieTouchData: PieTouchData( touchCallback: (event, response) { if (response null || response.touchedSection null) return; _onCategoryTapped(response.touchedSection!.touchedSectionIndex); }, ), ), ) ListPieChartSectionData _buildPieSections(ListCategoryAggregate aggregates) { return aggregates.map((agg) { final isSelected agg.category.id _selectedCategoryId; return PieChartSectionData( value: agg.totalAmount.toDouble(), color: agg.category.color, radius: isSelected ? 56 : 48, showTitle: false, ); }).toList(); }有几个实现细节需要说明第一showTitle设置为false。分类名称用图例列表展示饼图里不直接标注文字这样避免了饼图面积小的时候文字重叠的问题。这在分类多的情况下尤其明显比如用户有12个分类时小占比的扇区上根本容不下文字。第二touchCallback里根据触摸结果切换选中分类。这个交互视觉上很直观选中的扇区半径变大48变成56形成弹出来的效果同时联动下方的趋势图和顶部的筛选器。第三startDegreeOffset设置为-90度让一个扇区从正上方开始。这是数据可视化的一个基本习惯用户的视线起点一般在正上方从那里开始排布数据阅读效率最高。4.2 分类Top5列表的实现分类Top5列表在这个页面中承担的角色是快速定位花费大头。它用带进度条的列表形式展示每个分类的支出金额和预算执行情况。核心组件是一个自定义的CategoryListItemclass CategoryListItem extends StatelessWidget { final CategoryAggregate aggregate; final bool isSelected; final VoidCallback onTap; override Widget build(BuildContext context) { final budgetRatio aggregate.totalAmount / aggregate.category.budget; final isOverBudget budgetRatio 1; return ListTile( leading: CircleAvatar( backgroundColor: aggregate.category.color.withOpacity(0.15), child: Icon(categoryIconMap[aggregate.category.icon]), ), title: Text(aggregate.category.name), trailing: Text( ¥${aggregate.totalAmount.toStringAsFixed(2)}, style: TextStyle(fontWeight: FontWeight.w600), ), subtitle: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: budgetRatio.clamp(0.0, 1.0), backgroundColor: Colors.grey.withOpacity(0.1), color: isOverBudget ? Colors.red : aggregate.category.color, ), ), Text(isOverBudget ? 超出预算 ${((budgetRatio - 1) * 100).toStringAsFixed(0)}% : 预算剩余 ${((1 - budgetRatio) * 100).toStringAsFixed(0)}%), ], ), ); } }这个组件的核心是一个进度条。进度条的计算逻辑是支出金额除以预算金额超过100%时进度条颜色变成红色同时文字提示变成超出预算X%。这个逻辑在视觉上传达的信息非常直接用户不用看具体数字扫一眼进度条就能知道哪个分类快超支了。4.3 趋势折线图的实现趋势折线图展示的是近6个月指定分类或全部分类的支出趋势。这个图表我用了fl_chart的LineChart但在一开始踩了一个坑默认的LineChart在OpenHarmony上渲染的时候tooltip触摸提示框点击没有反应。排查后发现是fl_chart在OpenHarmony上缺少对PointerEvent的某些事件分支处理后来通过升级fl_chart到支持OpenHarmony的分支解决了。趋势图的实现代码LineChart( LineChartData( minX: 0, maxX: 5, minY: 0, maxY: _calculateMaxY(), lineBarsData: [ LineChartBarData( spots: _buildTrendSpots(), isCurved: true, gradient: LinearGradient( colors: [color.withOpacity(0.8), color.withOpacity(0.2)], ), barWidth: 3, dotData: FlDotData(show: true), ), ], titlesData: FlTitlesData( bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { // 根据索引返回月份标签 }, ), ), leftTitles: AxisTitles( sideTitles: SideTitles(showTitles: true, reservedSize: 40), ), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), ), ), )这里的关键参数是maxY的计算。如果不显式设置fl_chart会默认取数据最大值的1.2倍左右但在某些数据边界情况下折线的顶部会贴近图表上边界视觉上显得很局促。我通过_maxY maxValue * 1.3来预留空间。折线图还有一个交互细节值得提当用户点击饼图的某个分类后趋势图会切换为只显示该分类近6个月的支出情况。这个联动逻辑放在状态管理里通过Provider维护一个selectedCategoryId变量饼图和趋势图都监听这个变量的变化来刷新数据。4.4 筛选器与数据联动分类筛选器在这个页面里是一个横向滚动的FilterChip列表。每个分类一个Chip点击后触发数据联动更新。SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildFilterChip(全部, _selectedCategoryId null), ...categories.map((cat) _buildFilterChip(cat.name, _selectedCategoryId cat.id)), ], ), )联动逻辑是这样的点击全部饼图显示所有分类的占比趋势图显示所有分类的总额趋势点击某个分类饼图高亮该分类其他分类透明度降低趋势图切换为显示该分类的独立趋势这个联动逻辑一开始放在build方法里直接写结果每次刷新UI都重新计算聚合数据分类多的时候能感觉到卡顿。后来把聚合计算移到了ChangeNotifier里只有在筛选变化时才重新计算UI刷新只负责消费数据性能问题就解决了。4.5 状态管理与性能优化状态管理在这个页面里承担了数据聚合和UI状态两种职责。这两者要分开不然混合在一起非常痛苦。我的做法是定义了两个类class AnalysisData extends ChangeNotifier { ListBillRecord _bills; ListCategory _categories; DateTime _selectedMonth; String? _selectedCategoryId; ListCategoryAggregate get aggregates _computeAggregates(); ListBillRecord get filteredBills { if (_selectedCategoryId null) return _bills; return _bills.where((b) b.categoryId _selectedCategoryId).toList(); } Listdouble get monthlyTrend _computeMonthlyTrend(); } class AnalysisPageState extends StateAnalysisPage { override Widget build(BuildContext context) { return ConsumerAnalysisData( builder: (context, data, child) { return Column( children: [ _buildMonthSelector(data), _buildSummaryCard(data), _buildPieChart(data), _buildCategoryList(data), _buildTrendChart(data), ], ); }, ); } }这样做的核心好处是build方法只负责根据当前状态渲染UI所有复杂的数据计算都在ChangeNotifier内部完成而且通过notifyListeners触发UI更新时只有真正依赖该数据的组件才会重建。在性能优化上还做了一个操作饼图和趋势图组件用RepaintBoundary包起来。因为图表绘制本身就是Canvas操作如果父组件重建导致图表也重建每次刷新都要重新绘制OpenHarmony上的渲染开销会比Android端稍高RepaintBoundary可以把图表的Canvas绘制缓存成纹理只有数据真正变化时才重新绘制。5. 常见问题与排查技巧实录5.1 OpenHarmony模拟器与真机的显示差异开发阶段我主要用DevEco Studio自带的模拟器调试发现了一个很诡异的问题模拟器上分类分析页面的字体渲染偏小卡片间距也偏紧但真机上显示正常。排查后发现是模拟器的默认dpr设备像素比跟真机不一样。OpenHarmony模拟器默认dpr是1.0而大多数真机是2.0或者3.0。Flutter根据dpr调整逻辑像素的渲染导致同样的布局在模拟器上看起来偏小。解决方案不是改代码而是调模拟器配置将模拟器的分辨率从720p调到1080p或1440pdpr会相应变化显示效果就跟真机接近了。这个经验对于刚开始用OpenHarmony开发Flutter应用的朋友比较关键不然会为一个假问题浪费很多调试时间。5.2 fl_chart在OpenHarmony上的兼容性fl_chart是目前Flutter生态里比较活跃的图表库它在OpenHarmony上的适配情况还不错但有几个细节需要注意第一较早的fl_chart版本在OpenHarmony上存在手势冲突问题。主要表现是滑动饼图时页面同时发生滚动交互体验很差。原因是fl_chart内部的GestureDetector和ScrollView的手势竞争机制在OpenHarmony上表现得不够完善。升级到0.63.0以上版本后这个问题基本消失了。第二折线图的tooltip在OpenHarmony上需要额外的GestureDetector包装才能正常触发。这个问题在我使用的0.60版本中是必现的升级后消失。第三PieChart的一次性大数据量渲染比如1000条record在OpenHarmony上会比Android慢20%左右。这个差距在分类分析页面的场景下不明显但如果图表数据粒度很细建议在数据层提前聚合而不是把所有原始数据直接塞给图表。fl_chart的这些问题在官方GitHub的OpenHarmony issue里都有记录动手前先查一下issue能少走不少弯路。5.3 真机调试时adb连接不稳定的处理在OpenHarmony真机上调试Flutter应用adb连接不稳定是一个常见问题。如果你在开发过程中遇到device not found或者connected but running offline这类提示大概率是adb的版本和OpenHarmony设备的USB连接协商出了问题。我的处理经验先重启adb服务adb kill-server然后adb start-server检查USB调试权限是否授权在OpenHarmony设备上确认USB调试开关是打开的换一根数据线或USB口如果多次排查仍然连接不稳定建议用DevEco Studio自带的Device File Manager看能不能识别设备。如果DevEco Studio能识别而Flutter的命令行工具不能那大概率是adb版本不兼容手动指定OpenHarmony SDK中的adb路径来使用/Users/xxx/command-line-tools/sdk/default/openharmony/toolchains/adb devices5.4 分类数据不一致的排查经验在开发期间遇到过一个比较隐蔽的bug同一个分类在饼图上的金额和列表中显示的金额不一样。比如餐饮在饼图上显示1200元在Top5列表中显示1250元差额正好是50元。排查过程是这样的第一步打印聚合数据和列表数据对比原始账单记录第二步发现有一笔50元的餐饮消费同时出现在10月的聚合结果和11月的聚合结果中第三步检查日期过滤逻辑发现是时间边界没处理好10月31日的记录因为时区问题被计算到了11月这个问题的根源是DateTime的时区处理。当用户手机时区设置为非东八区时时间戳转换的日期可能跟预期不同。修复方式是在聚合计算时统一指定时区或者干脆存日期字符串而不是DateTime对象// 用字符串存储日期避免时区转换问题 class BillRecord { final String dateStr; // 格式yyyy-MM-dd DateTime get date DateTime.parse(dateStr); }这个改动看起来很小但从此之后跨时区的日期边界问题再也没有出现过。5.5 页面切换卡顿的优化分类分析页面还有一个性能问题是切换Tab时偶尔出现卡顿尤其是在选择了特定分类后切走再切回来。分析后发现页面在初始化和数据刷新时都做了重复的聚合计算。虽然聚合逻辑本身不算复杂但数据量增大的时候比如用户记了一年以上的账几千条记录重复计算就会造成可感知的卡顿。优化方案是给聚合结果加上缓存在ChangeNotifier里保存最近一次的计算结果只有当数据源或筛选条件变化时才重新计算ListCategoryAggregate? _cachedAggregates; String? _cacheKey; ListCategoryAggregate get aggregates { final key $_selectedMonth-${_selectedCategoryId ?? all}; if (_cacheKey key _cachedAggregates ! null) { return _cachedAggregates!; } final result _computeAggregates(); _cacheKey key; _cachedAggregates result; return result; }这样处理之后页面的切换响应时间从几百毫秒降到了肉眼无感的程度。6. 本项目的深度思考与扩展建议6.1 Flutter for OpenHarmony的生态观察整个项目做下来一个很深的感受是Flutter for OpenHarmony的生态在快速完善但跟Android/iOS相比还是有差距。具体体现在插件支持上像fl_chart这种纯Dart库直接用没问题chrome_audio、shared_preferences这类需要原生桥接的插件就要特别注意版本兼容。如果你在项目里用到了某个插件但OpenHarmony上跑不起来有两条路可以走第一找该插件的OpenHarmony适配版。社区里有一些志愿者维护的fork比如shared_preferences_ohos这类。使用时要确认fork的版本和稳定性。第二自己写PlatformChannel桥接。这个方式需要了解OpenHarmony的Native开发基础Ability、ACE等对于不熟悉鸿蒙原生开发的同学来说成本稍高。但对于核心业务依赖的插件自己桥接是更可靠的方案自己的代码卡脖子总比别人的库卡脖子好。6.2 数据可视化在理财场景中的设计原则回到分类分析这个页面本身想分享几点在设计数据可视化时的经验。第一信息要有层级。一个页面上同时展示环形图、柱状图、折线图很容易让用户晕。我的处理方式是环形图是主视图用户第一眼看到钱花在哪了Top5列表是补充视图用户看到排名和预算趋势图是进阶视图用户看到变化趋势。三者之间有主次关系而不是平铺。第二交互要保持一致。比如筛选器选中的分类在饼图上有高亮在列表里有背景色在趋势图上有颜色统一。所有这些视觉反馈必须对应同一个状态不然用户会困惑自己到底选了什么。第三数据要可追溯。图表展示的是聚合数据用户可能对某个数字有疑问此时应该能下钻到明细。我在列表项上加了一个查看明细的点击事件点击后弹出半屏的账单记录列表用户可以看到该分类的所有消费记录。6.3 从分类分析页面到完整理财App的扩展分类分析页面是整个理财App的一个重要模块但并不是全部。如果从项目的角度来规划后续可以考虑这样几个扩展方向预算设置与提醒在分类分析的基础上添加预算设置功能当某个分类支出超过预警线时通过本地通知提醒用户多账本支持家庭账本、个人账本切换分类分析的数据源跟着账本走报表导出把月度分类分析结果导出为图片或PDF方便用户分享或存档智能建议基于历史消费数据给出一些简单的消费建议比如本月餐饮支出比上月增加20%建议适当控制这些扩展方向在技术上都能复用当前页面的架构模式数据模型抽象、聚合计算、可视化组件、状态管理。只要前期的数据模型设计得足够通用新增功能时不会太伤筋骨。7. 写在最后几个我踩过坑后沉淀下来的小建议这个项目从启动到产出第一版大概花了三周时间。不算快但考虑到这是一个新平台上的探索性项目节奏还算合理。有几个经验想分享一下第一个建议是版本锁定。Flutter for OpenHarmony的组合是个动态变化的生态Flutter版本、OpenHarmony SDK版本、DevEco Studio版本之间有兼容矩阵。开发过程中一定要锁定这三个版本遇到问题时可复现、可排查。我项目里用的是Flutter 3.7.12 OpenHarmony SDK 4.0 DevEco Studio 4.0这几个版本配合起来是稳定的但如果你升级其中任何一个就要重新验证全套流程。第二个建议是图表库不要贪新。图表库版本更新很频繁但新版本带来的API变更如果没及时适配到OpenHarmony上很容易出现编译错误或运行时异常。我在项目中期升级过一次fl_chart结果导致饼图手势失效排查花了两天时间。后来明白了项目稳定运转时不要轻易升级依赖。第三个建议是在写状态管理之前先把数据模型设计好。我在这个项目里最大的重构成本就来自数据模型当初设计得不够好Map满天飞类型安全几乎为零。如果让我重来一次我会先花半天时间把实体类定义清楚把聚合算法的边界条件想明白再开始写页面。第四个建议是多跑真机别依赖模拟器。OpenHarmony模拟器的渲染行为跟真机的差异比Android的模拟器/真机差异更明显尤其是在图表渲染和字体适配两个维度。有条件的话最好在开发初期就借一台开发板或者真机很多问题早期就能发现而不是等到最后联调才暴露。分类分析这个页面做完之后我个人对OpenHarmony上跑Flutter的信心明显增强了。性能和渲染能力都能达到商业应用的要求剩下最大的变量是生态的完善度。相信随着更多开发者进入这个领域用Flutter统一开发Android、iOS、OpenHarmony多端的方案会越来越成熟。
返回列表