不想开会员,我写了一个记账 App

三个月、1300 多次提交,和一个不上云的 Android 账本

薄荷记账我用了挺久。免费版日常够用,界面也舒服,但有些自定义功能需要开会员。后来试了一木,功能很多,我只是想记一笔账,看到那么多入口有点累。

找了一圈,没有特别合适的。我是做前端的,平时也在学 Flutter,就想着自己写一个。最开始只是给自己用,没打算发布。

后来功能一点点补起来,就开源了。

现在这个 App 叫 Veri Fin,只有 Android 版,没账号、没广告、没云同步,代码开源。

这篇不逐项介绍功能,仓库和 README 里都有。我想写的是,这段时间为什么做成这样,以及我在技术上做了哪些选择。

为什么自己写

我的需求不复杂:数据放在自己手里,功能不要太多,界面干净一点。

一开始是做给自己,后来觉得别人可能也用得上。免费、本地、开源,多一个人用也不会增加什么成本,就发出来了。

我没做的几个功能

账号

没有做账号。

记账数据很私人,每天吃什么、买什么、收入多少,都在里面。放到服务器上,我不太放心。数据一旦交出去,后面会被怎么使用,用户也很难控制。

广告

有广告商联系过,我没接。

这个软件我自己每天都在用,不想打开先看广告。另外,一个靠广告赚钱的记账软件,说自己不会动用户的数据,也很难让人相信。

自动记账

自动记账考虑过,最后没做。

它一般需要屏幕识别、通知监听或者无障碍权限。这些权限能拿到的信息,比记一笔账多得多。我不确定自己能处理好这些风险,所以放弃。

现在的替代方案是截图识账:

flowchart LR A[分享/选择截图] --> B[端上 OCR] B --> C[识别文本] C --> D[用户自配的 AI 端点] D --> E[待确认草稿] E --> F{用户点击保存?} F -- 是 --> G[(本地 SQLite)] F -- 否 --> H[丢弃]

图片在本机做 OCR,只把识别出的文字发给用户自己配置的 AI,生成草稿。原图不上传,草稿也要用户确认才保存。

比自动记账麻烦,但数据边界清楚。

AI 后端

AI 没有做自己的后端。

免费软件承担 token 成本不现实,用户越多支出越高。现在由用户配置兼容 OpenAI 的接口,Key 存在本地,请求从设备直接发出。

用户需要自己准备 Key,门槛高一些,但成本可控。

技术选择

我不是 Flutter 专家,也没有做过完整的技术选型。很多决定只有一个标准:以后一个人还能不能维护。

现在的结构大概是这样,页面不直接访问数据库,写操作统一经过 Controller 和 Repository:

flowchart TB UI[Flutter 页面] --> C[VeriFinController] C --> R[LedgerRepository] R --> DB[(SQLite)] C --> KV[(SharedPreferences)] C -. 偏好通知 .-> UI UI --> P[Platform Bridge] P --> A[Android 原生能力]

结构不复杂,排查问题时比较容易定位。

Flutter

我是前端出身,RN 本来更熟。当时 RN 的环境和依赖问题比较多,加上想学 Flutter,就选了 Flutter。

后来也考虑过用 RN 重写,后面会讲。

Flutter 对我的主要帮助是 UI 统一。间距、圆角、颜色和标题高度定好之后,几十个页面可以按同一套规则实现。

SQLite

数据层最初考虑过 JSON 文件。

后来算了一下,我每天记五六笔,一年大约两千笔,再加上账户、分类、预算和汇率,JSON 每次都要整体读写,数据量上来之后不合适。

所以第一版用了 SQLite。现在 schema 到 v19,迁移、备份、附件和账本隔离都建立在它上面。

交易表大概长这样,只贴了跟多币种有关的字段:

sql 复制代码
CREATE TABLE entries (
  id                TEXT PRIMARY KEY,
  book_id           TEXT NOT NULL,
  type              TEXT NOT NULL,
  amount            REAL NOT NULL,               -- 原币金额
  currency_code     TEXT NOT NULL DEFAULT 'CNY', -- 交易币种
  account_amount    REAL,                        -- 账户实际扣款
  to_account_amount REAL,                        -- 转账转入实际金额
  base_amount       REAL NOT NULL DEFAULT 0,     -- 冻结的本位币金额
  conversion_source TEXT NOT NULL DEFAULT 'legacy',
  -- 其余字段略:分类、账户、备注、时间、标签、退款关联......
);

多币种不能只加一个币种字段。原币金额、账户实际扣款和记账时冻结的本位币金额要分开存,否则汇率变化会影响历史数据。

状态管理

状态管理我一开始不熟悉。Provider、Riverpod、Bloc 都听过,但没有实际用过。

项目最初的骨架由 AI 生成,我确认能运行、能读懂之后继续开发。最后使用的是 Controller + InheritedNotifier:

dart 复制代码
class VeriFinScope extends InheritedNotifier<VeriFinController> {
  const VeriFinScope({
    super.key,
    required VeriFinController controller,
    required super.child,
  }) : super(notifier: controller);

  static VeriFinController of(BuildContext context) {
    final scope = context.dependOnInheritedWidgetOfExactType<VeriFinScope>();
    assert(scope != null, 'VeriFinScope not found');
    return scope!.notifier!;
  }
}

结构很简单,状态在哪里、谁修改、什么时候写库,都可以直接查到。

后来增加的测试和规范,主要是为了避免这个简单结构在功能变多后失控。所以没有使用现成的状态管理库,原因并不复杂:一开始不会,后来发现当前方案够用。

界面

第一版界面比较粗糙,参考了薄荷记账,信息放得比较多。

后来研究过液态玻璃,也给底栏做了可选的玻璃样式。但做到后面,还是觉得界面简单一点更好。

现在除了底栏,其他区域都使用纯色背景和实色卡片,不使用模糊、折射或整屏渐变。除了视觉考虑,也因为每增加一种材质,就多一批需要测试的界面状态。

AI

AI 工具全部只读。

dart 复制代码
abstract class AiQueryTool {
  String get name;
  String get description;
  AiToolSchema get schema;
  AiToolResult run(AiToolContext ctx, Map<String, Object?> args);
}

List<AiQueryTool> buildAiQueryTools() => <AiQueryTool>[
  const SummaryTool(),
  const CategoryRankingTool(),
  const QueryTransactionsTool(),
  const NetWorthTool(),
  const BudgetStatusTool(),
  // ...其余只读工具
];

接口没有保存、修改或删除操作,工具只能读取冻结的数据快照,不能访问 Controller。AI 可以查询、统计和生成图表,但不能修改账目。

记账草稿可以生成,保存仍然由用户完成。

桌面小组件

这段时间花时间最多的是桌面小组件,不是 AI。

它表面上是一张卡片,实际运行在 Flutter 之外的 Android 环境。不同启动器和系统版本的表现不同;应用进程被杀后,它还要显示上一次的快照;跨天时"今日支出"要归零;切换账本、语言或深浅色后,数据也要更新。

四个模板、实例配置、选择器预览和刷新时机,每个部分都有不少边界情况。Android 12 到 14 的选择器预览需要提前渲染 PNG,API 35 之后才支持生成预览。我写了一个脚本,在真机上导出 16 张预览图。

它大概有三种状态:

状态 桌面显示 点击后
未配置,或配置的账本已删除 一行"点击进入配置",不显示任何金额 打开配置页
已配置,但快照缺失或版本不符 一行"打开应用同步数据" 打开 App
已配置且快照有效 指标 + 金额 + 环/曲线 打开对应账本

这个模块做完之后,我开始补自动化测试。这些情况靠手动点击覆盖不了,今天修好一个启动器,明天可能在另一个系统版本上出问题。项目只有我一个人,CI 和测试承担了大部分回归检查。

关于重写

有一段时间想用 RN 重写。

一方面是前端出身,对 RN 更熟;另一方面是早期代码质量一般,看起来有很多可以改的地方。

后来算了一下时间,也问自己重写能解决什么问题。结论是主要改善我自己的开发体验,用户不会因此多记一笔账。当前代码不漂亮,但功能是稳定的。为了重写让用户等几个月,不合适。

所以没有重写。

AI 与开发

以前我不确定 AI 能不能辅助完成一个完整产品。现在的经验是,可以,但前提是项目有明确的规范。

项目初期的骨架由 AI 搭建,我确认后,后续相当一部分代码也由它完成。为此我写了很多约束:数据结构、页面骨架、组件复用、持久化时机、颜色规则和确认流程。

AI 的执行速度很快,但约束越模糊,生成的代码越容易停留在"能运行"的程度。能不能在几个月后继续维护,是另一回事。

所以关键路径都补了测试:迁移、仓储、备份恢复、小组件布局。三个月 1300 多次提交,其中很多是工具完成的重复工作。

不过 AI 只能写代码,不能替我做产品决定。自动记账要不要做、广告接不接、界面有没有变复杂,这些都需要我自己判断。

三个月之后

三个月,1300 多次提交。写这篇时,仓库有 167 个 star、21 个 fork,46 个 Issue 已关闭,3 个仍开着。

这些数字不算大,但说明确实有人在用,也有人愿意反馈。

开源协议使用 GPL-3.0。代码可以查看、学习和修改,但不希望被闭源打包销售。准确说,GPL 不禁止商业使用,它要求衍生作品同样开源。

不接受 PR,只处理 Issue。项目规范比较多,外部 PR 很难一次对齐。与其在 review 中反复修改,不如把需求说明白,由我按现有规范实现。这个方式不一定适合所有项目,但目前适合我。

没有商业化。广告联系过,没有接;捐赠入口有,但不是主要收入来源。这个项目首先要保证我自己愿意每天使用。

最大的变化是对做产品的理解。

以前觉得独立开发最重要的是执行力,现在觉得写代码正在变得便宜。AI 可以快速完成页面、测试和错误排查,几天就能做出一个可运行的版本。更难的是决定做什么、不做什么,以及什么时候停止。

所谓 IDEA,落到实际,就是一系列具体决定:不做账号,不做广告,不做屏幕监听;用 SQLite 不用 JSON;让用户自己配置 API Key;在想重写的时候停下来。

这些决定组合起来,才是现在这个产品。

如果你也有类似的需求,建议先把它做成自己每天愿意用的工具,不必一开始就考虑产品化。边界想清楚,规范写下来,能自动化的部分尽量自动化。AI 可以提高速度,但方向仍然要自己判断。

关于 Veri Fin

一个完全免费、开源、数据自主的 Android 记账应用。

感谢你的阅读,如果想要体验软件,欢迎下载!如果想要讨论相关技术,提出建议等,也可以提出 Issue 或者在评论区讨论!

相关推荐
2601_949575867 小时前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间7 小时前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭9 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇1 天前
Dart 之模式
android·flutter·dart
淡淡的香烟1 天前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓1 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter
程序员老刘2 天前
Flutter版本选择指南:3.44稳定了11周正式转正 | 2026年10月
flutter·ai编程·客户端
chicken_h2 天前
【无标题】
flutter·打包
事圆则缓2 天前
Flutter 底层渲染引擎怎么变了:从 Skia 到 Impeller,以及迁移时的坑
flutter