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

0 阅读9分钟

三个月、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,迁移、备份、附件和账本隔离都建立在它上面。

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

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:

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 工具全部只读。

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 可以查询、统计和生成图表,但不能修改账目。

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

微信图片_20261026135002_7_31.jpg

桌面小组件

这段时间花时间最多的是桌面小组件,不是 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 或者在评论区讨论!