
1. 为什么单独把“分类列表页面”拿出来写做理财App的人都知道功能再花哨用户打开后最先接触的往往不是图表曲线而是“我的账本里到底有哪几类钱”。分类列表页面表面上看就是一行行分类名称加上图标实际上它决定了用户后续所有记账、筛选、统计行为的入口。我这次用Flutter for OpenHarmony做个人理财管理App第一个完整落地的页面就是这个分类列表页过程中踩的坑比预想的多不少所以特意整理一篇实战记录。先说说为什么这个页面值得单独写。理财App里分类列表要承担的职责远不止“展示”。它需要支持自定义分类、计算每个分类下的支出占比、承载侧滑编辑和删除操作还要在列表顶部给出本月的分类花费概览。很多团队第一版图省事用最简单的ListView加几个静态卡片糊弄过去结果后续加统计报表、加多级分类时全部要返工。数据结构没设计好、列表状态没理清后面每一步都在还债。这篇内容适合正在用Flutter做跨端应用、偶尔需要面向OpenHarmony设备适配的开发者看也适合准备做理财类、记账类工具产品的人作为页面结构参考。我会尽量把从数据模型、页面骨架到真实设备调试中遇到的坑都摊开讲代码能直接用思路能迁移到其他业务页面上。2. 项目背景与跨端选型2.1 为什么在这个项目里用Flutter做OpenHarmony应用市面上做OpenHarmony原生界面有自己的一套ArkUI和方舟开发框架团队如果从零开始学成本不算低。而我们当时的实际情况是核心团队长期用Flutter写跨端业务已有的账本模块、图表模块、UI组件库都是Flutter写的如果换成纯ArkUI从头写基本上等于放弃既有复用资产。所以选择了Flutter for OpenHarmony这条路线让同一个Dart代码仓库既能出Android/iOS版本也能出OpenHarmony版本。这里有个关键点要解释清楚Flutter for OpenHarmony并不是把Flutter的Android引擎直接搬过来而是某技术社区维护了一套适配鸿蒙内核的Flutter引擎SDK开发者在工程里通过替换引擎依赖、配置对应的toolchain来跑Dart代码。对我这种业务开发来说最大的感受是Flutter的渲染层、手势层、布局系统在OpenHarmony设备上基本是完整可用的Dart代码的写法和平常几乎没有区别但涉及到平台插件、纹理渲染、系统字体等底层能力时会明显感受到适配层还不像Android/iOS那么成熟。2.2 工程结构里最容易被忽略的配置创建Flutter for OpenHarmony工程时第一步不是急着写代码而是要把SDK分支切到对应的OpenHarmony适配版本。如果你用普通的Flutter SDK打包通常只能编出Android的产物连OpenHarmony的hap包都出不来。建议直接在工程内部建好ohos目录把OpenHarmony侧的entry模块和插件模块都放在这个目录下同时保持lib目录里的Dart代码不感知平台差异。另外Dart侧的pubspec.yaml依赖选择上要避免无脑拉最新版插件。很多纯Dart包没问题但带原生代码的包比如路径选择、设备信息、本地数据库驱动都需要确认是否已经针对OpenHarmony做了适配。我这次因为拉了一个没适配的旧版SharedPreferences编译时直接报符号链接错误排查了大半天最后才发现是插件版本把Android的so文件也带进了OpenHarmony工程。所以依赖管理这里优先选支持extension的版本或者干脆用Channel自己封装反而省时间。3. 分类列表页的数据层设计3.1 分类实体应该包含哪些字段很多新手设计分类表时只放一个分类名加一个图标名后面做统计时候发现完全不够用。我设计的数据模型除了基础字段外还加了几个容易被忽略的属性。class CategoryModel { final String id; final String name; final String icon; final int sort; final int type; // 0: 支出, 1: 收入 final String parentId; final bool isSystem; final String remark; CategoryModel({...}); }其中parentId必须留好虽然第一版只做一级分类但理财场景里很容易延伸出“餐饮-早餐”、“餐饮-外卖”这种二级结构如果一开始没有预留父子关系后面改表结构要迁移历史数据非常痛苦。isSystem用来区分系统预置分类和用户自定义分类系统分类不允许删除只能重命名或隐藏用户自定义分类可以随意增删。这个规则在产品侧很常见代码里一定要有对应的判断逻辑。sort字段的价值也很容易被低估。分类列表不是字典序排列而是应该按照用户的使用频率排序。我在编辑页做了一个拖拽排序的功能每次调整后把新的sort写回本地数据库列表读取时按sort升序排列体验上接近原生系统设置页的排序方式。3.2 本地存储是选择数据库还是文件缓存分类数据量其实很少一个家庭账本用户分类总数最多几十条感觉上随便存个文件就够了。但我仍然建议用SQLite这种结构化存储原因是分类和账单记录有关联关系删除分类时要判断该分类下是否有账单有的话要提示用户“将账单归入其他分类”或“级联删除”。这种关系型查询用文件缓存来做代码会变得非常绕。项目里使用的是某通用Dart数据库库来操作本地SQLite先用一张category_table存储分类再用一张bill_table存储账单记录。分类列表页只需要读category_table但删除分类时会去bill_table做一次关联统计。数据库表里加了一个biz_code字段这个字段不是业务编号而是防止不同月份或不同账本的分类互相污染查询时始终带着bizCode条件避免因为隔离条件忘了带把A账本的分类显示在B账本里。3.3 状态管理在这个页面里怎么收敛分类列表页涉及的状态不算多无非是分类列表数据、加载状态、是否处于编辑模式但如果你不做约束代码会很快失控。我用比较轻量的状态管理方案只拆了三个Store一个管分类集合一个管当前选中的分类一个管编辑面板的显隐状态。分类集合的Store里暴露addCategory、updateCategory、removeCategory这三个Mutation列表页只在用户手势操作后调用这些方法不直接修改底层List。这里我吃过一次亏列表页用了下拉刷新之后直接对旧列表做clear()再addAll()造成列表组件重建时闪烁同时滚动位置悄悄回到顶部。后来全部改为增量更新或者给列表数据加key区分版本才解决了刷新跳动的问题。4. 列表页面的UI结构与分类列表实现4.1 页面骨架从概览卡片到分类列表的层次我做的页面结构分三块顶部返回栏和标题、月度分类消费概览卡片、下方分类列表主体。顶部概览卡片是一个横向滚动区域展示“本月总支出”“分类数量”等汇总指标这部分不是核心但能提升页面整体信息密度。列表主体才是重点。Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(分类管理)), body: Column( children: [ OverviewCard(monthlyStats: monthlyStats), Expanded( child: RefreshIndicator( onRefresh: _loadCategories, child: ListView.builder(...), ), ), ], ), ); }列表主体我没用CustomScrollView原因很简单这个页面的滚动场景相对单一没有复杂的Header折叠效果ListView.builder配合itemExtent设置固定高度性能足够且逻辑简单。很多人一上来就铺大组件其实查一下列表是否需要吸附、是否需要交错动画大部分场景用最基础的列表就够了。4.2 列表项如何设计才能兼顾信息密度和操作便利列表项的UI结构是最左侧是分类图标中间是分类名称和备注信息右侧是本月花费金额和占比。右侧金额下面加了一条小进度条视觉占比约25%用不同颜色区分支出和收入分类这个小进度条带来了很好的质感用户不用进入统计页就能直观感受钱花在哪个大类比较多。Widget _buildListItem(CategoryModel model) { return ListTile( leading: CategoryIcon(icon: model.icon), title: Text(model.name), subtitle: Text(${model.billCount}笔账单), trailing: Column( children: [ Text(${model.monthAmount}元), LinearProgressIndicator(value: model.percent), ], ), ); }Flutter for OpenHarmony在ListTile上表现正常但要注意的是leading区域如果用了网络图标就不要把加载逻辑直接塞进build方法否则列表滚动时会连续触发图片加载导致肉眼可见的掉帧。我这边所有分类图标都是本地资源用Image.asset直接加载渲染速度很快。如果你要做用户自定义远程图标建议用缓存库统一处理。4.3 下拉刷新与上拉加载怎么在OH设备上保持顺畅下拉刷新用的是RefreshIndicator在OpenHarmony真机上表现基本稳定可能是我的分类数据量小没有触发太复杂的动画线程竞争。但上拉加载这里我做了个更实用的选择分类列表一屏基本能装下大部分分类所以没有做分页加载而是用“加载更多”的静态提示做尾部队列。除非用户创建了超过50个分类否则一次性渲染完整列表反而避免分页状态带来的割裂感。需要注意的是如果之后要把这个列表扩展成“账单分类筛选列表”即一个分类下包含几十条账单那时就不能用静态列表了需要改成ScrollController监听底部位置再动态加载。我在代码里保留了scrollController和hasMore字段就是为了后续扩展时不用推翻重写。5. 编辑、侧滑、排序等交互的落地细节5.1 右侧编辑面板从底部弹出的分类编辑表单列表页右下角有一个悬浮按钮点击后从底部弹出编辑面板。这个面板用来新增分类也用来在点击某个列表项后复用编辑详情。面板里包含分类名称输入框、图标选择器、类型切换支出/收入、备注输入框。这里我踩的第一个坑是底部弹层在OpenHarmony上默认没有键盘避让点击输入框后键盘会遮住表单下半部分导致看不到“保存”按钮。解决方式是监听MediaQuery.of(context).viewInsets当键盘弹出时给底部弹层内容区加一个内边距同时用AnimatedPadding做过渡动画。如果你用的Flutter版本较旧viewInsets在某些设备上可能不更新那就需要自己监听系统键盘高度Channel。我这边测试下来某版本的适配SDK已经能正确处理键盘避让但保险起见还是加了主动监听。5.2 侧滑删除时业务规则先行删除分类不能一刀切如果分类下面已经有账单记录直接删除会导致账单变成“无头数据”。我实现的侧滑删除使用的是Dismissible组件在确认删除回调里先查账单数量如果数量大于0就弹对话框让用户选择“迁移账单”或“连同账单一起删”。这个操作不是单纯前端判断而是要先调用数据层方法拿到关联数量再决定弹窗文案。Dismissible在OpenHarmony上有个小问题滑动手势偶尔会和外层RefreshIndicator的垂直滚动冲突表现为左右滑动列表时有时会先触发下拉刷新的阻尼效果。排查后确认是SDK的手势竞技场处理还不够完善我通过给Dismissible的direction限定为DismissDirection.endToStart并给RefreshIndicator设置notificationPredicate优先让水平方向手势生效基本解决了冲突。5.3 拖拽排序的实现要点分类排序用的是可重排列表组件里面按sorted值展示分类。拖拽时我长按列表项进入排序模式此时隐藏侧滑按钮同时把正在拖动项的背景色提亮。排序完成后逐条更新数据库里的sort字段这里不是把所有项都重写一遍而是只记录排序中的两个分类的下标变化再批量执行更新减少数据库写入次数。拖拽排序在OpenHarmony真机上有个明显的体验差异部分设备默认开启的触控反馈会和拖拽的移动逻辑抢占事件导致拖动时不跟手。实测下来给DragTarget加上一个delay参数延迟50毫秒进入拖拽态同时调整拖拽过程中重叠项的动画时长跟手感和流畅度会好很多。6. 打通OpenHarmony特性与Flutter层的适配工作6.1 图标资源、字体、状态栏的适配应用在OpenHarmony上运行默认字体是系统字体族如果你之前只适配了Android的Roboto或iOS的PingFang SC到OH设备上可能会发现部分符号渲染偏窄。我这边用了Flutter的fontFamilyFallback依次指定HarmonyOS Sans和PingFang SC这样在OH设备上优先调用系统字体在Android上回退到原字体整体视觉统一。状态栏的处理也和Android不同。OpenHarmony的状态栏高度在部分设备上会和Flutter的SafeArea存在偏差表现为页面顶部出现白条或内容被遮挡。我直接在MaterialApp.builder里包了一层MediaQuery.withClampedTextScaling再结合SafeArea(top: true)控制页面主体避免每个页面重复处理状态栏。如果你接的是设备厂商定制ROM可能还要对接系统侧的状态栏高度Channel这里不能只依赖Flutter默认值。6.2 本地数据文件路径的差异分类图标和用户头像这类资源如果从网络下载需要缓存到本地。在Android上缓存路径通常取getCacheDir()在OpenHarmony上则不同。我项目里用的路径是通过PathProvider插件获取的但OpenHarmony适配的PathProvider在某些版本下返回的目录没有末尾分隔符导致拼路径时写成了xxx目录文件名直接连在一起日志上看到的就是文件路径不存在。解决方法是统一封装一个PathUtil所有路径拼接都用p.join工具避免手写字符串拼接并且对返回的根目录先做一次是否存在判断不存在则自动创建。这个小改动一开始不起眼但在用户设备上能避免大量头像加载失败的问题。6.3 桌面端布局的意外收获与隐患原本做的是手机页面但Flutter for OpenHarmony在平板上调试时页面会自动拉伸分类列表的ListView.builder在宽屏上会撑满整行视觉上很别扭。我用LayoutBuilder做了一个最大宽度约束当屏幕宽度大于600dp时列表内容居中显示在最大宽度为560dp的区域内左右留白。这个处理对平板和折叠屏打开应用都很重要不然列表项里的金额占比条会被拉得过宽比例失衡。隐患在于宽屏适配后下拉刷新的notificationPredicate需要同步调整某些平板设备上报的手势坐标会被拉伸导致下拉触发区域偏移。我定位到问题是DPI适配没走Flutter引擎的自动缩放需要在ohos模块里检查设备的displayId和缩放比例这属于底层设备配置普通业务开发很少碰到但遇到时不能慌。7. 真机调试与性能优化实录7.1 编译、热重载、日志查看的日常流程OpenHarmony设备接入后的日常调试我用的是DevEco工具链配合Flutter命令通过flutter attach进行热重载。但有一个明显区别Flutter在Android上的热重载几乎是秒级生效在OpenHarmony上偶尔会变成全量重建尤其改了ohos目录下原生代码后只能重新打hap包。起初不习惯后来我都是先改Dart代码热重载原生适配的改动集中到一个批次再一起重新安装这样效率最高。查看日志方面Dart侧的print输出在命令行能看到但OpenHarmony系统侧的原生日志需要连接设备查看系统日志总线。建议从第一天就在代码里封装一套带tag的日志工具把业务日志和系统日志分开过滤不然几百条原生线程日志里找一条业务错误真的很折磨人。7.2 列表滚动性能怎么测才真实分类列表页的滚动性能我用的是Flutter性能面板记录帧率并特意在动画面板开启的状态下连续滚动列表50秒观察是否有jank。由于列表项里包含进度条和图标首帧渲染可能会有轻微卡顿我优化了布局进度条组件改为CustomPaint绘制避免用LinearProgressIndicator隐藏的布局开销列表项外面包一层const构造让不可变组件复用Element节点。真机上还要注意发热问题。OpenHarmony设备的性能调度在某些场景偏保守跑长时间动画后系统会主动降低主频导致之前流畅的操作掉帧。我处理办法是在下拉刷新和拖拽排序这类高频率操作结束后主动释放控制器资源同时避免在动画循环里创建新对象。性能和体验优化是持久战不追求一次到位但要保证主流设备不卡。7.3 内存回收与页面释放我的分类列表页本身内存占用不大但进入编辑面板后如果连续新增、删除、再新增分类旧的分类图标缓存可能一直驻留在内存里。Flutter的ImageCache默认容量比较大小图标加载几百张可能都不触发回收。我在页面销毁时主动清掉了与分类图标相关的缓存并确认scrollController被dispose避免页面切走再返回时因为旧监听导致崩溃。这个习惯要养成尤其是你在OpenHarmony上做应用系统侧对后台应用的内存回收比Android更严格。页面没释放干净的常见表现是后台回来后列表滚不动、或点击事件无响应。日志里往往看不到DefiniteError只会看到卡顿和丢帧排查起来特别费时间。8. 常见问题排查速查表现象可能原因处理方式编译时报so文件冲突插件依赖了非OH架构的二进制查看插件版本排除或替换为OH适配版列表滚动时上下抖动RefreshIndicator手势与Dismissible冲突限定Dismissible方向配置notificationPredicate底部弹层被键盘遮挡键盘避让未生效监听viewInsets并设置AnimatedPadding拖拽排序不跟手系统触控反馈抢占事件加延迟进入拖拽态调整DragTarget参数路径拼接找不到文件PathProvider返回路径格式差异统一使用路径拼接工具并自动创建目录宽屏平板上列表拉伸未做最大宽度约束用LayoutBuilder限制内容宽度居中显示页面后台回来不响应控制器资源未释放页面销毁时dispose控制器清理图片缓存这个小表格是排错时的快速捷径但每条背后都有一个完整的定位过程。实际开发里表里的“可能原因”不会一下精确命中需要有耐心做最小化复现。我一般是先写一个空页面逐步加代码复现后再对照上面的方向排查效率比直接在业务页面里猜高得多。9. 这个页面后续可以怎么扩展分类列表页后续最大的改动点会来自多账本。我现在只支持一个默认账本如果要做“家庭账本”“个人账本”多个账本切换分类数据就不能只靠bizCode区分需要引入账本维度并且每个账本可以有独立的分类体系。列表页顶部要加上账本选择器分类增删改查都要携带账本Id。这个改动对页面的影响不是局部函数级别的而是数据层到表现层都要贯穿。另一个可扩展方向是智能推荐分类。通过记录用户每月新增分类的名称和图标做去重聚类后自动推荐“可能想创建的分类”列表页底部会多一个推荐区域用异步加载避免阻塞主列表。这块逻辑和自动记账可以联动但需要服务端数据支持不适合纯客户端项目。分类和预算联动也是理财App常见演进方向在分类列表页每个分类右侧显示“本月预算剩余额度”超出后分类背景色微红提示。这会引入预算表结构和额度计算逻辑页面本身改动不大核心在内存计算和更新时机上建议在稳定版之后再做。就我自己的实践体验来说分类列表页这种“看起来简单、做起来琐碎”的页面恰恰是练内功的好地方。数据模型设计多花一小时后面能省三天填坑。跨端适配没有银弹关键在于提前认清哪些地方是你的业务代码、哪些地方是适配层的职责边界边界清楚了问题就好定位。最后再分享一个小技巧所有设备相关的临时调试补丁都在代码里留一个isDebugDevice开关量产时统一关掉不然过了两个月你根本想不到为什么线上包会多出一个只在某台平板生效的逻辑。