做衣橱管理类App,大部分开发者第一时间想到的功能是衣物的增删改查、搭配推荐、换季整理提醒,预算管理经常被排在次要位置。但我自己实践下来的结论恰恰相反:预算管理如果不放在数据模型阶段一起设计,后面再想补,成本会翻好几倍。这篇文章就把我在Flutter for OpenHarmony项目里落地衣橱管家App预算管理模块的经验完整过一遍,重点讲清楚表结构、本地数据库适配、月度额度计算、以及最终在RK3568开发板上真机验证时遭遇的问题。整套内容适合已经在Flutter上有一定基础、准备把应用移植到OpenHarmony平台,或者正在做工具类应用但不知道本地数据怎么设计的开发者参考。
1. 为什么预算管理不是“统计附属”,而是衣橱数据的核心脉络
很多衣橱类产品把预算做成一个独立页面,用户买完衣服回来手动记一笔账,应用只负责把当月总额相加。这个方案初看简单,实际用起来非常别扭,因为用户根本不会定期记。真正合理的模型是:预算管理应该挂在采购行为上,有衣物入库,就自动产生一笔支出记录,预算模块只是从支出记录里做聚合计算。这样用户不需要“记账”,只需要“添加衣物”,数据就自然流动到预算统计里。
这个差异决定了整个表结构的设计方向。我见过不少团队先建了clothing_items表,后来要加统计功能,又新建了purchase_records表,结果两套数据经常对不上。因为用户可能修改一件衣物的价格,可能退换货,可能在一件事上重复录入两次,数据源一旦分散,对账就是噩梦。所以我在OpenHarmony版本里直接采用单一事实来源:每件衣物就是一件资产,它的purchase_date和purchase_price就是预算统计的唯一口径,不额外引入手工记账表。用户对衣物价格做了编辑,预算统计同步变化,逻辑天然一致。
这个设计思路也是我在Flutter for OpenHarmony项目初期踩过一轮坑以后才定下来的。最初受限于赶工,我在UI层直接对衣物列表做遍历求和,换来换去页面多了以后,性能问题开始出现,而且计算口径不统一——列表页用的是实时总额,详情页用的却是缓存数字,两边差了二十多块钱,排查了半天才发现是数据来源不一致。后来我把预算计算全部下沉到数据库层的聚合操作,UI只负责读取结果。
这套模式带来的额外好处是:OpenHarmony设备通常内存和CPU性能不如旗舰手机,如果衣物的图片列表再加上循环求和,页面掉帧会非常明显。用SQL聚合把计算量压到最小,对中低端开发板尤为重要。
2. Flutter工程接入OpenHarmony目标平台时的初始化选择
先把背景说清楚。OpenHarmony自身推荐的应用开发语言是ArkTS和ArkUI,但我们团队选择Flutter,核心原因是已有代码库积累和跨平台人力复用。Flutter针对OpenHarmony的适配工程已经可以跑通基础渲染和大部分原生能力调用,社区称之为Flutter for OpenHarmony,配置完成后能用同一套Dart代码在不同系统构建。
工程初始化阶段有几个细节需要特别确认清楚,否则越到后面越难受。
第一,Flutter SDK版本要和OpenHarmony SDK版本尽量保持对应。OpenHarmony侧的三方适配通常跟着Flutter稳定版走,别再追Flutter最新beta频道。我见过有同事直接切到beta版本,结果构建工具链提示libflutter_engine.so与系统镜像不匹配,整体回退浪费了一整天。
第二,创建Flutter工程以后,不要急着写业务代码。先到ohos目录下检查build-profile.json5和hvigor-config.json5,确认compileSdkVersion和compatibleSdkVersion与目标设备固件的API Level一致。API Level不一致,轻则部分系统能力调不通,重则安装阶段直接被拒绝。
第三,第三方插件兼容性问题。Flutter的插件体系依赖平台通道,OpenHarmony适配工程提供了自己的平台实现,很多插件还不能直接用。我们的原则是:能用Dart层解决的,绝不依赖原生插件;必须用原生能力的,优先找OpenHarmony社区替代品。预算管理模块就是典型——它完全可以在Dart和本地数据库层实现,不需要第三方平台通道。
在依赖配置上,我最终只引入了三个关键依赖:sqflite_common_ffi用于本地数据库,intl用于日期和金额格式化,fl_chart用于月度趋势图表。这个组合够轻,也没有踩到奇怪的鸿蒙插件坑。另外用path_provider时要注意,OpenHarmony的文件目录和Android路径逻辑不完全一样,最好统一封装一个路径管理函数,方便后续适配。
从实际操作经验来说,初始化阶段最重要的一句话就是:先把最小Flutter应用跑到OpenHarmony模拟器或真机上,再谈业务。很多人在工程都还没跑通的时候就开始写界面,最后平台能力调不通,经济成本和情绪成本都拉满。
3. 预算模块的表结构与数据关系:把“预算”变成可统计的数据流
衣橱管家App的预算管理围绕四个维度的数据在转:衣物资产数据、采购支出数据、预算计划数据、品类分类数据。我在数据库设计上做了四张表,它们之间的主外键关系是整个模块稳定性的基础。
clothing_items表:保存每件衣物的基础信息和采购信息,这是预算统计的主表。字段如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER PRIMARY KEY AUTOINCREMENT | 衣物ID |
| name | TEXT | 衣物名称 |
| category_id | INTEGER | 关联品类表 |
| purchase_price | REAL | 购买价格,预算统计直接依赖此字段 |
| purchase_date | TEXT | 购买日期,格式YYYY-MM-DD |
| store_name | TEXT | 购买渠道,可选字段 |
| remark | TEXT | 备注 |
| created_at | INTEGER | 创建时间戳 |
budget_categories表:预算分类表,我建议至少拆成上衣、裤装、裙装、外套、鞋靴、配饰六类。分类是为了让预算不是一锅粥,用户可以针对外套单独设月度上限。
budget_plans表:预算计划表,记录用户在某个周期内给某个品类设定的预算额度。
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER PRIMARY KEY AUTOINCREMENT | |
| plan_month | TEXT | 预算月份,格式YYYY-MM |
| category_id | INTEGER | 品类ID,-1表示全部品类 |
| limit_amount | REAL | 当月预算上限 |
| updated_at | INTEGER | 更新时间戳 |
purchase_records表,我开始设计时纠结过要不要这张表。最后保留它,但做了减法:它不保存冗余的价格数据,只记录衣物与预算月份的关联关系。因为衣物价格可能事后修改,如果这个表里冗余存了一个快照价格,反而容易和衣物主表冲突。所以这张表只保存clothing_id、plan_month、record_date,统计时通过join clothing_items来取实时价格。
这套结构的优势是:衣物价格改了,统计自动变;用户删掉一笔误录入的衣物,支出统计同步减少;预算表格干净,没有脏数据。
在数据库初始化阶段,我用onCreate做表结构创建,同时插入默认品类数据。这里有个小经验:不要用onUpgrade去频繁改表结构,因为OpenHarmony设备上用户升级应用时数据库迁移一旦出错,恢复成本很高。我建议在正式版本前尽量把表结构一次性定稳,必要的话预留扩展字段。
4. 本地数据库连接OpenHarmony的关键一步:sqflite_common_ffi适配
我最早尝试直接用Android生态非常成熟的sqflite插件,但在OpenHarmony上很快卡住了。sqflite依赖Android平台的SQLite原生实现,而Flutter for OpenHarmony的通道体系还无法无缝承接这个插件的原生调用。翻遍社区后发现,最务实的方案是sqflite_common_ffi。
sqflite_common_ffi的思路是在Dart层通过FFI调用SQLite的动态库,绕开平台通道,因此它天然适配OpenHarmony。标准的sqflite在初始化时需要一个onDatabaseCreate回调,背后由原生端创建数据库文件;sqflite_common_ffi则是借助sqlite3动态库直接在Dart侧完成文件读写,所以只要目标系统里存在可用的SQLite库,或者我们可以直接打包一个SQLite的动态库进去,就能工作。
实际操作时,需要先在main函数里初始化:
dart复制import 'package:sqflite_common_ffi/sqflite_ffi.dart';
void main() {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
runApp(const WardrobeApp());
}
然后在封装的数据访问层中,用openDatabase创建或打开数据库文件。OpenHarmony上文件的根路径和Android不太一样,我建议使用getDatabasesPath来获取当前应用的数据库目录,不要硬编码路径。
dart复制Future<Database> openWardrobeDb() async {
final dbPath = await databaseFactory.getDatabasesPath();
final path = '$dbPath/wardrobe.db';
return databaseFactory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE clothing_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
category_id INTEGER NOT NULL,
purchase_price REAL NOT NULL DEFAULT 0,
purchase_date TEXT NOT NULL,
store_name TEXT,
remark TEXT,
created_at INTEGER NOT NULL
)
''');
// 其他建表语句和默认数据
},
),
);
}
这里有一个关键点:sqflite_common_ffi在初始化时对数据库是否已存在有一套自己的判断逻辑,如果应用在启动时没有及时调用sqfliteFfiInit(),后续的openDatabase会直接抛错。这也是很多人在OpenHarmony上试了半天本地数据库一直起不来的常见原因。
还有一个小坑是事务。预算统计需要频繁执行插入和聚合查询,我建议所有写操作显式放到db.transaction里,不要依赖自动提交。OpenHarmony历代系统对数据库的I/O调度有自己的策略,事务化能有效避免偶发的“数据库被锁定”报错。我实际测试下来,加上事务处理以后,连续写入几十条衣物的耗时稳定在百毫秒级,完全可接受。
5. 月度预算计算逻辑:从“已消费金额”到“剩余额度”再到超支预警
预算管理模块的核心计算其实不复杂,但容易写得不严谨。我先说一下正确口径,再贴核心代码。
计算某月所有品类的已消费金额,不能简单把clothing_items里purchase_date以当月开头的记录求和,因为这里存在两个边界条件:用户可能补录历史衣物,可能一次性录入下个月的预售衣物,还有可能在后半夜录入导致日期偏移。我的做法是,只统计purchase_date在当月1号到当月最后一天之间的记录,且排除掉状态为“已归档”或者“已删除”的数据。为了支持这个逻辑,我在clothing_items表里加了一个is_deleted软删除字段,预算统计默认只算未删除记录。
已消费金额的聚合查询如下:
dart复制Future<double> getMonthSpent(String month) async {
final startDate = '$month-01';
final endDate = getLastDayOfMonth(month);
final result = await db.rawQuery(
'''
SELECT COALESCE(SUM(purchase_price), 0) AS total
FROM clothing_items
WHERE is_deleted = 0
AND purchase_date >= ?
AND purchase_date <= ?
''',
[startDate, endDate],
);
return (result.first['total'] as num?)?.toDouble() ?? 0.0;
}
得到已消费金额后,再查预算计划表拿当月总上限:
dart复制Future<double> getMonthLimit(String month) async {
final result = await db.rawQuery(
'SELECT limit_amount FROM budget_plans WHERE plan_month = ? AND category_id = ?',
[month, -1],
);
if (result.isEmpty) return 0.0;
return (result.first['limit_amount'] as num).toDouble();
}
单品类预算的算法在此基础上加一个category_id筛选即可。剩余额度就是limit - spent,超支状态在spent > limit时触发。
这个逻辑看起来简单,但实际我试过很多花哨写法,比如在Dart层把所有衣物拉出来再循环累加,数据量小的时候没问题,一旦衣物条目超过几百条,页面性能肉眼可见下降。后来我坚持把所有计算都下沉到SQL层,用SUM和COALESCE处理空值,Dart层只接收一个数字。第二个坚持是金额用REAL而不是整数分,虽然浮点精度在极端情况下可能有小数点误差,但中国币种只到分,浮点数做普通统计完全够用,而且避免了大数变字符串的尴尬。
超支预警我用了一个简单但有效的判定策略:不是只在用户打开预算页时才判断,而是在每次新增或编辑衣物并写入数据库后,立刻重新统计当月已用金额,再和额度对比。如果超支,就弹一个非阻塞的提示条,而不是弹窗打断录入流程。这个交互决策很重要,因为用户添加衣物时情绪是正面的,一旦被弹窗打断,体验会很差。
6. 预算录入与月度看板:把数据变化直观呈现在页面上
预算录入的页面我没做复杂的表单,而是用了一个BottomSheet配合日期滚轮。用户点击“添加衣物”时,依次填写名称、选择品类、输入价格、选择购买日期,保存后自动写入数据库并刷新预算看板。整个过程控制在15秒以内,减少用户填写成本。
录入页面核心代码骨架:
dart复制class AddClothingSheet extends StatefulWidget {
final Database db;
final String month;
const AddClothingSheet({Key? key, required this.db, required this.month})
: super(key: key);
@override
State<AddClothingSheet> createState() => _AddClothingSheetState();
}
class _AddClothingSheetState extends State<AddClothingSheet> {
final _nameController = TextEditingController();
double _price = 0;
int? _categoryId;
String _purchaseDate = DateTime.now().toString().substring(0, 10);
Future<void> _save() async {
if (_nameController.text.isEmpty || _categoryId == null) return;
await widget.db.insert('clothing_items', {
'name': _nameController.text,
'category_id': _categoryId,
'purchase_price': _price,
'purchase_date': _purchaseDate,
'created_at': DateTime.now().millisecondsSinceEpoch,
'is_deleted': 0,
});
if (mounted) Navigator.pop(context, true);
}
}
这里有个必须处理好的细节:BottomSheet保存后返回值为true,父页面根据返回值决定是否刷新预算统计。我用await showModalBottomSheet获取返回值,返回true就重新查询数据库并刷新图表。如果返回值没有透传,页面数据就会停留在旧状态,用户以为保存失败了,这种体验问题在真机上特别容易被举报。
月度预算看板我实现了三块内容,保持信息密度适中:
- 顶部是当月“已用金额 / 预算额度”的进度条,超支时进度条变红,并显示“超支XX元”。
- 中部是六个品类的预算使用卡片,每个卡片显示品类名、已用金额、品类剩余额度。这个模块帮助用户快速定位是哪个品类超支。
- 底部是一个近六个月的柱状图,横轴是月份,纵轴是支出金额,用
fl_chart绘制。柱状图能直观看出衣物消费的季节性趋势,用户换季时特别需要这个数据。
fl_chart的柱状图渲染在OpenHarmony真机上表现还可以,但如果条目特别多,动画会有点掉帧。我的建议是:只在用户滑动到图表区域时才触发动画,或者直接关掉动画,渲染速度会快很多。另外,中文数字和单位格式化用intl包非常顺手,但如果只是简单场景,可以直接用字符串拼接,少一个依赖就少一分适配风险。
7. RK3568开发板真机验证:设备树选择、hdc连接和性能复盘
到了真机验证这一步,我遇到了比预想更多的环境问题,其中最有代表性的就是OpenHarmony的RK3568设备树选择。RK3568是一个很常见的芯片平台,但对应OpenHarmony的固件和开发板适配文件非常多,很多开发者打开下载页面会直接看花眼。这里我的建议是:先确认清楚自己手上的开发板具体型号和硬件版本,再去找配套的固件包和dtb设备树文件。如果是常见的标准评估板,直接用官方默认配置即可;如果是第三方厂商定制板,一定要找厂商确认对应的设备树文件名,否则可能开不了机或者外设不识别。
设备树这块和App开发者的关系,不像内核开发者那么直接。但有一个场景绕不开:你在烧录完系统镜像后,需要用hdc工具连接设备调试,如果设备树选错导致USB外设或者网络驱动没起来,hdc就死活连不上。所以我的建议是,在准备OpenHarmony开发环境前,先把开发板配套文档里关于设备树的部分截图保存,调试时一旦出现连接问题,优先排查这个因素。
hdc连接和Android的adb命令非常类似,常用的几个命令如下:
bash复制hdc list targets
hdc shell
hdc file send
hdc install com.example.wardrobe.hap
Flutter项目构建完成后,生成的产物是.hap包。这里注意,OpenHarmony的安装包格式是.hap,不是.apk,很多第一次接触的人会搞混。安装时如果遇到签名校验失败,需要检查工程里配置的签名证书是否和设备上开启的签名校验策略匹配。
真机性能表现上,RK3568跑Flutter应用整体可用,但算不上丝滑。预算看板页的列表滚动在优化前有轻微掉帧,我做了两件事之后明显改善:一是把列表项里的图片改成了延迟加载,只有滚动到可视区域才加载;二是把预算进度条的颜色渐变效果去掉,改用纯色填充。Flutter的渐变绘制在嵌入式设备上开销不低,这种视觉上的小让步换来了体感流畅度的提升,我觉得值。
另外还有一个数据库层面的优化值得提一下。预算统计页首次进入时,如果数据量很大,SQL聚合查询可能需要几十毫秒,虽然看起来不长,但在弱设备上会感觉到卡顿。我的做法是进入页面前先读取缓存显示,同时在后台异步重新查询,查询完成后再用setState刷新。这样用户感知到的启动速度更快,数据也能保证新鲜。
8. 一点补充分享:预算数据备份与多设备同步的扩展思路
写到这,顺便聊一下后续扩展方向。之前我提到预算数据全部存在本地数据库,但真正的衣橱管家App大概率会做账号同步。OpenHarmony平台有自己的分布式数据管理能力,但考虑到通用性,也可以考虑把本地SQLite数据同步到服务端。我的建议是不要一开始就做实时双向同步,复杂度太高。可以先做导出导入JSON文件,让用户自己备份和迁移,后面再考虑账号体系的云同步。
在数据导出上,我封装了一个简单的序列化逻辑:
dart复制Future<String> exportBudgetData() async {
final db = await openWardrobeDb();
final items = await db.query('clothing_items');
final plans = await db.query('budget_plans');
return jsonEncode({
'version': 1,
'export_time': DateTime.now().toIso8601String(),
'items': items,
'plans': plans,
});
}
导入时同理,解析JSON后先清空对应表,再逐条插入。这里有个原则要记住:如果预算统计已经依赖衣物表的价格数据,导入时必须先插入品类,再插入衣物,最后插入预算计划,否则外键关系会出错。
这套同步方案目前已经够用,而且不依赖任何OpenHarmony原生能力,纯Flutter代码全平台可跑。等到用户量上来以后,再考虑服务端合并冲突策略也不迟。
从整个项目复盘来看,Flutter for OpenHarmony的成熟度虽然不如Android,但做一个工具类应用已经足够务实。只要把数据库层设计稳、计算逻辑收敛到SQL端、真机验证时多关注嵌入式的性能瓶颈,预算管理这种功能模块完全可以做出不错的体验。希望这篇实战复盘能帮到正在做同样事情的人。
