用Flutter和AI辅助开发本地优先记账App的完整实践
1. 项目缘起与整体设计思路1.1 为什么自己动手写记账 App先说背景。我一直有记账的习惯但市面上的记账 App 用了一圈都不太顺心有的广告铺天盖地有的强制注册账号、数据全部上传云端有的功能堆得比企业 ERP 还复杂记一笔账要点五六次屏幕。我不是什么专业理财达人需求其实特别朴素——快速记下每笔开销、月底看看钱花哪了、别让数据裸奔在别人的服务器上。找了很久没找到完全满意的索性自己用 Flutter 写一个。这个项目对我个人的意义有两个一是解决真实需求二是我一直想实测一下 AI 辅助开发到底能提效多少。平时工作里用 AI 写点脚本、补个测试用例没问题但完整地从零开发一款 App让 AI 深度参与从架构设计到功能落地的全流程这是第一次。这个项目和很多练手项目不一样的地方在于它有一个完全明确的目标——做出来的东西我真的会天天用所以每个功能都不能是凑数的摆设。如果你也是 Flutter 初学者或者想尝试用 AI 提速开发但不知道怎么下手这篇文章应该能给你一些可复用的思路。我不会只贴代码更重要的是分享我踩过的坑、以及哪些环节 AI 能帮上大忙、哪些环节你必须自己把关。1.2 技术选型为什么是 Flutter 而不是别的选 Flutter 的理由很直接我要 Android 和 iOS 双端都能用但不想维护两套代码。Flutter 的跨端能力保证了 UI 一致性和开发效率一套 Dart 代码同时覆盖两个平台个人项目最缺的就是时间能省一半工作量对我就是决定性的优势。技术栈的选型也围绕个人项目维护成本最低这个原则来定模块选型理由跨端框架Flutter一套代码跑 Android/iOSUI 一致性好状态管理Riverpod编译期安全、依赖注入清晰AI 生成代码时也容易理解本地数据库driftSQLite轻量可靠支持关系查询和流式响应图表库fl_chartFlutter 生态里最成熟的图表方案之一网络请求dio功能全面拦截器做日志和安全处理方便纯本地优先数据默认只存手机里不上任何云。记账数据是高度隐私的我自己的原则是——除非万不得已不把账单交给第三方服务器。这也意味着整个 App 的核心就是一个本地数据库加上几个页面复杂度可控非常适合一个人加 AI 来搞定。1.3 记账 App 的核心功能拆解开始写代码之前我先把记账这件事拆成了最小可用版本必须有的功能记一笔金额、分类、账户、日期、备注这是记账这个动作本身的最核心闭环账单列表按日期分组显示账单能下拉查看历史记录每笔账单可编辑、可删除统计报表分类占比这个月吃饭花了多少、月度支出趋势、总支出/总收入概览这三个功能构成了记账 App 的最小闭环往里记数据、查看流水、分析数据。至于预算提醒、多账本、账单拍照识别、数据导出这些都属于后续迭代的加分项。一个个人项目最怕的就是一开始就想着做大而全最后做成一堆半成品功能。先把最小闭环跑通每天用起来再根据真实感受加东西这才是个人开发项目的正确节奏。2. AI 辅助开发实操怎么让 AI 真正帮你干活2.1 把需求讲清楚AI 才能干得漂亮AI 辅助开发这件事最关键的一步不是写代码而是写需求描述。很多人让 AI 写 Flutter 页面直接来一句帮我写个记账页面出来的东西能用吗能用但基本是花架子没有数据模型、没有状态管理、没有和数据库的联动本质是一个静态 UI 草稿。我的做法是给 AI 足够的信息密度。比如做账单列表页时我的 prompt 大概是这样的需求实现一个 Flutter 账单列表页面用 Riverpod 管理状态。 数据模型 Transaction 已有字段id、amount整数单位分、categoryId、accountId、 dateDateTime、noteString?、createdAt。 要求 1. 页面顶部显示本月总支出和总收入。 2. 列表按日期倒序分组同一天内的账单按创建时间倒序排列。 3. 每个列表项显示分类图标、分类名称、备注、金额正数为收入绿色负数为支出红色。 4. 下拉刷新重新加载数据。 5. 列表项支持左滑删除删除前弹出确认对话框。 数据统一从 TransactionRepository 中获取该 Repository 已有 loadMonthSummary 和 watchTransactionsByMonth 方法请直接调用不要重新实现数据库逻辑。这段 prompt 里包含了数据模型、功能点、交互方式、已存在的接口。AI 生成代码的可用率大幅提升因为它的想象空间被限制住了它不需要自作主张去设计数据库结构或者猜测状态管理方案只需要按照给定的接口实现页面逻辑。如果 AI 生成的代码不符合预期我一般不直接手动改完事而是把问题描述清楚再让它改。比如图表横轴标签重叠我会说当前饼图图例太多导致标签重叠请改成只显示前 5 个分类的图例其余合并为其他并加上切换交互——描述得越具体AI 的修改越精准。2.2 AI 擅长干什么把精力留给真正难的事做完整个项目之后我复盘了一下 AI 在这类个人项目里的高光时刻:UI 布局生成Flutter 的组件嵌套有时候很繁琐一个列表项套着圆角卡片、行布局、列布局手写容易漏括号。AI 生成这类结构代码几乎不会犯错而且速度快到离谱。CRUD 模板代码增删改查相关的代码模式非常固定AI 基本能一次写对。单元测试补全让 AI 为数据层写测试用例它能把正常路径、边界值、错误输入都覆盖到这一点比我手工写还细致。报错信息翻译与定位Flutter 的报错信息有时候很晦涩直接把报错全文丢给 AI 让它解释并给修复方案效率比搜索引擎高一大截。我自己的时间可以省下来去做 AI 做不了的事梳理业务逻辑、设计数据库表结构、规划数据安全策略、打磨交互细节。2.3 AI 最容易翻车的三个环节AI 虽然强但在几个特定环节翻车率相当高。这是我的第一手体会数据库迁移。AI 生成 drift 的建表代码没问题但涉及到给已有的表加字段、改字段类型、做数据迁移AI 很容易给出激进方案——直接 drop table 重建这会导致用户数据全部丢失。个人记账 App 的数据丢了等于白用所以所有迁移脚本我都会逐行审查绝不直接跑 AI 生成的迁移代码。跨页面状态同步。AI 在同一页面内部的状态管理通常处理得很好但一旦涉及记一笔页面保存后统计页面要立刻刷新这种跨模块状态同步AI 生成的代码经常出现逻辑漏洞比如忘了 invalidate 对应的 Provider导致数据改了但界面不更新。Flutter 版本相关的 API 差异。AI 的训练数据里混着不同 Flutter 版本的 API 用法有时会生成老版本的写法比如用已经被废弃的WillPopScope而不是PopScope或者用已经被移除的RaisedButton。这个问题通过报错信息反馈给 AI 可以修正但需要你有能力判断报错是版本问题而不是代码逻辑问题。这三个环节我的策略很明确AI 可以参与但最终决定权必须在自己手里。每一段涉及数据安全和版本兼容的代码我都会亲自读一遍再合入。3. 核心功能实现详解3.1 数据模型设计一个坑两种方案记账 App 的第一个关键设计决策是金额用什么类型存储。直觉上可能用double但浮点数在金融场景里是大忌——0.1 0.2 不等于 0.3累计多了误差就出来了。我的方案是金额一律用整数存储单位是分。// drift 表定义 class Transactions extends Table { IntColumn get id integer().autoIncrement()(); IntColumn get amount integer()(); // 单位分正数收入负数支出 IntColumn get categoryId integer().references(Categories, #id)(); IntColumn get accountId integer().references(Accounts, #id)(); DateTimeColumn get date dateTime()(); TextColumn get note text().nullable()(); DateTimeColumn get createdAt dateTime().withDefault(currentDateAndTime)(); }展示的时候再转换amount / 100得到元。这样既避免了浮点误差也能保证统计聚合的精确性。表结构上用了三张表transactions账单流水、categories分类、accounts账户。分类和账户单独建表的原因是有后续扩展空间——分类要支持自定义图标和颜色账户要支持余额和类型再往后还可能做多币种。如果一开始就把分类做成一个字符串存进账单表里后面改起来会非常痛苦。3.2 记一笔的完整流程记一笔是整个交互里最核心也最考究的页面。用户需要完成的动作是输入金额、选择分类、选择账户、确认日期默认今天、填备注。我的目标是把这个流程控制在 5 次点击以内完成。金额输入我用的是一个自定义的数字键盘界面而不是系统键盘因为系统键盘会弹出英文输入法、符号键等干扰项还容易误触。自定义键盘上只有 0-9、小数点、删除键和完成键布局尽量接近微信红包的输入体验用户不需要切换输入法输入效率高很多。保存逻辑上我用了 Riverpod 的StateNotifier管理当前草稿账单的状态保存成功后invalidate账单列表的查询 Providerclass TransactionDraftNotifier extends StateNotifierTransactionDraft { TransactionDraftNotifier(this._repository) : super(TransactionDraft.empty()); final TransactionRepository _repository; Futurebool save() async { final amountInCents formatAmountToCents(state.amountText); if (amountInCents null || amountInCents 0) { return false; // 金额为0或格式非法时不保存 } await _repository.insertTransaction( amount: amountInCents, categoryId: state.category!.id, accountId: state.account!.id, date: state.date, note: state.note.trim(), ); return true; } }保存成功后通过ref.invalidate(transactionsProvider)让列表和统计自动刷新。这里的关键是 Riverpod 的自动依赖追踪——只要列表和统计依赖同一个数据源 Providerinvalidate 一次所有依赖它的界面都会自动重新加载不需要手动去通知各个页面刷新。这个机制在跨页面数据同步上帮了大忙。3.3 账单列表与月度统计账单列表的核心诉求是按月查看流水所以数据库查询直接按月份过滤然后按日期倒序排列。为了让列表分组我按日期字符串分组渲染同一天的多笔账单归到一个 section header 下。月度汇总不是在前端循环累加而是在 SQL 层面聚合FutureMonthlySummary loadMonthSummary(DateTime month) { final start DateTime(month.year, month.month, 1); final end DateTime(month.year, month.month 1, 1); final query db.select(db.transactions) ..where((t) t.date.isBiggerOrEqualValue(start) t.date.isSmallerThanValue(end)); final rows await query.get(); var income 0; var expense 0; for (final row in rows) { if (row.amount 0) { income row.amount; } else { expense row.amount.abs(); } } return MonthlySummary(income: income, expense: expense); }统计页用 fl_chart 画饼图和柱状图。饼图展示分类支出占比柱状图展示近 6 个月支出趋势。这里我建议把图表的 label 做一下限制分类一多图例就乱前 5 个分类展示真实名称剩下的合并为其他视觉清爽也更有可读性。性能方面月度汇总的数据量其实不大直接内存聚合完全够用。但如果你导入的历史数据特别多比如一次导入了上万条可以考虑把聚合逻辑放到 isolate 里去执行避免 UI 卡顿。我在下一节详细说说这个问题。4. 开发环境与常见报错排查实录4.1 创建 Flutter 项目时遇到的 VS Code 工具链报错把这个项目落地到 Windows 环境后我遇到的第一个大坑是 VS Code 里创建 Flutter 项目后运行 Android 模拟器构建时直接报错unable to find suitable visual studio toolc。这个报错字面上是找不到 Visual Studio 工具链但它真正的问题不是你没有装 VS而是 Flutter 的 Android 构建在 Windows 上需要 C 桌面开发工具链来编译某些原生插件。我一开始以为装了 VS Code 就够了但实际上需要的是 Visual Studio 的 Build Tools 组件。解决办法是安装 Visual Studio Build Tools然后在单个组件里勾选Windows 11 SDK和MSVC v143 - VS 2022 C x64/x86 生成工具这两个关键项。装完之后重开 VS Codeflutter doctor就会从报错变成通过。4.2 Gradle 插件应用方式的警告与版本管理项目创建后使用新版 Flutter 模板时Android 构建出现了这样一个警告you are applying flutters main gradle plugin imperatively using the apply script method, which is deprecated and will be removed in a future release这是新版 Flutter 在推动 Gradle 插件从命令式应用迁移到声明式应用的过渡提示。旧的模板是在android/settings.gradle或android/build.gradle里用apply方法加载 Flutter 插件新的推荐方式是在插件的plugins代码块里声明。遇到这个警告可以直接创建一个新版 Flutter 项目把android/settings.gradle里的插件声明方式同步过来。不需要过度纠结警告本身但如果你发现某些 Flutter 插件在老项目里编译出问题时这个版本差异往往是根源。多版本 Flutter 管理我推荐用 FVM。我这个项目用 Flutter 3.x 稳定版另外一个老项目还在用 2.x直接用 FVM 切版本非常方便fvm install 3.24.0 fvm use 3.24.0 fvm flutter doctor用 FVM 之后所有flutter命令都要通过fvm flutter来执行VS Code 里也要把 Dart SDK 路径指到 FVM 对应的 Flutter SDK否则 IDE 和命令行用的还是同一个版本切换就没有意义了。4.3 内存优化与 Isolate导入历史账单卡顿的解决办法记账 App 的常规操作不会出现性能问题但我遇到一个真实场景——朋友给了我一堆历史账单数据要一次性导入大概 5000 多条。在最开始的实现里我在 UI 线程直接解析 CSV 并逐条插入数据库App 直接卡了 2 秒多期间界面完全无响应体验非常糟糕。Flutter 是单线程模型所有 UI 操作都在 main isolate 上执行。你在 main isolate 里做大量计算或大量数据库写入界面的每一帧渲染都会被阻塞。解决办法是把耗时任务放到单独的 isolate 里执行final importedCount await Isolate.run(() async { final rows await parseCsvFile(filePath); // 在后台 isolate 解析 CSV return rows; });Dart 的Isolate.run是 Dart 3 引入的简化接口它会自动在新 isolate 里执行传入的函数然后把结果传回主 isolate。在 isolate 里做完解析再回到主 isolate 里分批插入数据库。600 条一批每批插入后await Future.delayed(Duration(milliseconds: 1))让出时间片UI 就能保持流畅。实测下来界面完全不卡总导入时间反而从 2 秒多降到 1.5 秒左右因为 UI 线程不再被阻塞不会出现 ANR 的风险。另外列表长列表渲染时记得用ListView.builder而不是一次性生成所有 item。ListView.builder是懒加载模式只渲染当前可见区域的 item内存占用稳定。个别新手容易直接写ListView(children: [...])然后循环生成所有 item数据量一上来就直接内存爆炸。4.4 dio 请求封装与抓包问题这个项目虽然以本地存储为主但我在做后续 AI 能力扩展时接入了网络请求用到了 dio。个人项目里做网络层时最值得花时间的是统一封装而不是每个页面各写各的。我封装的 dio 实例包含三样东西基础超时设置连接超时和接收超时都设 15 秒、日志拦截器debug 模式下打印请求和响应信息、统一错误处理把 DioException 转成业务错误码UI 层不用到处 catch。抓包调试是另一个刚需。Android 模拟器抓 HTTPS 包需要安装证书而且要确认 Android 系统版本是否信任用户证书——Android 7.0 之后默认只信任系统证书。一个快速调试方案是 debug 模式下的 AndroidManifest 里允许明文流量!-- android/app/src/debug/AndroidManifest.xml -- application android:usesCleartextTraffictrue /这仅限调试包正式发布包绝对不要开明文流量否则会有严重的安全隐患。4.5 运行时连接本地服务失败的排查开发过程中遇到过启动 App 时抛异常提示Couldnt start the app because http://127.0.0.1:7860/gradio_api/。这个报错跟 Flutter 本身没关系是应用里某个功能尝试连接本地的 Gradio 服务一般是 AI 模型服务但服务没有启动。这类问题的排查思路很直接确认服务是否在运行、确认端口是否对应、确认手机上访问电脑的 IP 是否正确Android 模拟器里 127.0.0.1 指向的是模拟器自己不是你的电脑需要改成 10.0.2.2 才能访问宿主机。如果是真机调试则要填电脑在局域网里的实际 IP。4.6 常见报错速查表报错信息根本原因解决办法unable to find suitable visual studio toolcWindows 缺 C 构建工具链安装 Visual Studio Build Tools勾选 C 桌面开发组件applying flutters main gradle plugin imperativelyFlutter 版本与 Gradle 插件声明方式不匹配按新版模板改成声明式插件应用Couldnt start the app because http://127.0.0.1:7860/...应用内依赖的本地服务没有启动确认服务地址可访问区分模拟器/真机的地址差异java.lang.UnsatisfiedLinkErrorNative 插件编译或兼容问题确认插件版本、清理并重新构建Execution failed for task :app:mergeDexDebug依赖冲突或方法数超限检查依赖版本启用 multidex如果项目足够大5. AI 在项目中的边界哪些环节必须人工把关5.1 数据安全记账数据默认不上云用 AI 辅助开发带来的一个隐患是开发者容易放松对数据安全的敏感度。AI 给出的方案里经常默认带一个云同步功能——建议把用户数据同步到云端方便多设备查看——但个人记账数据是非常敏感的交易记录万一服务端泄露后果比丢失手机严重得多。我的决策是默认纯本地存储所有数据只留在 SQLite 数据库里。如果未来真的要做备份或跨设备同步也一定优先考虑端到端加密方案服务端只保存密文不保存明文。这个底线不能因为 AI 的默认建议而动摇。5.2 依赖版本与兼容性问题AI 在给出代码方案时会根据训练数据里的知识推荐依赖库。但它的知识库更新有滞后性会给出版本比较老的库或者推荐一个已经不再维护的库。每次 AI 推荐新依赖我都会做两件事一是查这个库在 pub.dev 上的最新版本和更新时间二是确认它与我当前 Flutter 版本兼容。实际上 fl_chart 和 drift 这两个核心库我都是自己查文档定的版本AI 推荐的版本要么太低、要么和当前 Flutter 冲突。这类决策还是得靠人AI 替代不了。5.3 业务逻辑的边界条件AI 生成的代码往往覆盖了正常流程但对异常场景的处理不够周全。比如删除一个分类时这个分类下已经记录了很多笔账单——是禁止删除、级联删除还是把账单改为未分类AI 默认选择最简单的级联删除把历史数据全删了这在账本场景里是不可接受的。记录负数金额、跨天时间处理、时区转换、闰年 2 月 29 日的日期选择这些看起来不起眼的边界条件恰恰是用户真实使用中最容易出 bug 的地方。我给 AI 的任务描述里会反复强调边界条件——如果金额为 0 应该怎么处理、如果分类已删除的旧账单如何展示——把边界问题前置说清楚比事后修 bug 高效得多。6. 发布前的最终检查清单开发完成到可以真正装上手机日常使用还要过一遍发布检查。个人项目虽然不比企业级产品但该守的底线一条都不能少。权限最小化这个记账 App 实际只需要存储权限如果要做备份功能。不需要通讯录、不需要定位、不需要相机后期拍照识别另说。每申请一个权限都要问自己这个权限真的必要吗启动速度冷启动时间实测控制在 1.5 秒以内启动页不要搞花哨的动画直接进主界面。数据备份虽然我的方案是纯本地存储但我加了手动导出 JSON 的功能。用户隔一段时间导出一次备份就算手机丢了数据还能找回来。这是个人记账 App 最重要的一个功能比花里胡哨的图表重要得多。崩溃监控个人项目没有条件上完整的 APM 系统但至少要在 Debug 模式下接一个简单的全局错误捕获记录崩溃日志到本地文件方便排查。我用的是FlutterError.onError加PlatformDispatcher.instance.onError双通道捕获确保 Dart 层和 Native 层异常都能记录下来。说一个我实际遇到的发布问题Android App 打包 release 版时忘记配置签名导致安装后一直提示应用未安装。这个问题在本地 debug 跑不会出现因为 debug 包用的是 debug 签名但 release 包必须用自己的签名文件。建议创建项目后第一时间配置好 release 签名别等到打包前再搞。具体来说就是在android/app/build.gradle里配置signingConfigs然后生成一个.jks密钥文件并妥善保管——一旦丢失已经发布的应用永远无法用同一个签名升级。7. 写在最后AI 辅助开发的正确定位这个记账 App 从立项到实现核心功能上线前后用了大概三周其中 AI 参与了大约 60% 的代码量。如果没有 AI我估计要花五到六周。但关键是省下来的时间并不是AI 替我写代码这么简单而是AI 处理了重复劳动我把精力集中在真正需要判断力的事情上。我个人体会最深的一点是AI 辅助开发要想提效前提是你自己清楚想要什么。如果你对需求一知半解AI 生成的代码就是一堆看似正确但拼不起来的功能碎片如果你对数据模型和状态管理有清晰的设计AI 就像一支执行力很强的外包团队你说清楚需求它就能交付出可用的东西。这个项目后续我还在继续往下做下一步计划是加入语音记账——直接说午饭 25 块AI 自动识别金额和分类生成一笔账。这个功能网上的方案不少正好用我之前积累的 AI 接口封装来接应该不会太麻烦。回头做完我会再写一篇实操记录把语音识别、关键词分类这些踩坑经历也补上。如果你也在用 Flutter 和 AI 折腾自己的小工具欢迎在评论区聊聊你遇到的坑——也许你踩过的那个坑恰好是我接下来要踩的。