当前位置: 首页 > news >正文

Flutter Web + Supabase 构建 AI 家计簿:从原型到全功能模块的实战

1. 项目概述:从128行原型到全功能AI家计簿的蜕变

最近在做一个挺有意思的项目,我们团队在开发一个叫“自分株式会社”的AI生活管理应用,目标是把Notion、Evernote、MoneyForward、Slack这些你日常用的21个SaaS工具,全都整合到一个地方。这想法听起来有点野心,但做起来确实能解决信息碎片化的大问题。就在前几天,看到亚马逊的Rufus AI推出了“Buy for Me”功能,能帮你分析购买决策,这让我突然意识到,我们手头的家计管理模块还只是个128行的静态展示页面,实在太简陋了。作为一个财务管理和效率工具的聚合平台,没有点智能化的家计分析功能,实在说不过去。于是,我决定用Flutter Web,在几天内把这个“摆设”页面,彻底重构成一个带有AI节建议和未来资产模拟的、真正能用的家计AI顾问。

这个新页面的核心目标很明确:不仅要能像MoneyForward那样清晰地记录和分类收支,更要利用AI去理解你的消费模式,主动给出省钱的实操建议,并且能让你直观地看到,如果坚持某个储蓄或投资计划,未来5年、10年你的资产会变成什么样。最终,我把一个原本只有几个数字卡片的页面,扩展成了包含4个核心标签页、超过750行代码的完整功能模块。整个过程没有增加新的后端服务,完全复用现有的AI能力,用纯Dart实现了复杂的财务计算,并且保持了代码库的绝对整洁(flutter analyzedeno lint都是0警告)。如果你也在用Flutter做Web应用,并且想引入AI能力或处理复杂的业务逻辑,我踩过的坑和总结的模式,或许能帮你省下不少时间。

2. 架构设计与技术选型背后的思考

2.1 为什么是Flutter Web + Supabase组合?

选择Flutter Web作为前端,对我们来说几乎是必然的。我们的核心应用是跨平台的,一套代码能跑在移动端和Web端,维护成本大大降低。Flutter Web经过几个大版本的迭代,现在的性能和体验已经足够支撑这种数据密集型的后台管理页面。渲染图表、频繁更新状态(比如用户调整预算滑块时实时更新进度条)都很流畅。更重要的是,Flutter丰富的UI组件库和高度自定义的能力,让我们能快速构建出体验一致且美观的财务数据看板。

后端选择Supabase,则主要基于其“一体化”和“无服务器优先”的特性。我们的应用涉及用户认证、实时数据、AI接口调用等多个层面。Supabase的Auth、Postgres数据库、Realtime、Storage以及Edge Functions,正好覆盖了所有这些需求。特别是Edge Functions,它让我们能用TypeScript或Deno快速部署无服务器函数,处理像AI对话这类需要调用外部API(如Anthropic的Claude)的敏感或复杂逻辑,而无需自己管理服务器。这次家计AI顾问的核心——节建议生成,就是直接复用了我们已有的一个通用ai-assistantEdge Function,实现了零成本的功能扩展。

2.2 数据层设计:通用表与源标识模式

在数据存储设计上,我们采用了一个非常灵活且节省资源的模式。通常,遇到“预算计划”、“实际支出”这类新功能,第一反应可能是创建budget_plansexpenses这样的专用表。但我们没有这么做,而是选择复用了现有的app_analytics通用事件表。

这个表结构很简单,核心字段有user_idtimestampsourcemetadata(JSONB类型)。source字段就是关键,我们用不同的字符串来区分数据用途。例如:

// 保存用户设定的2024年7月“餐饮”预算 await supabaseClient.from('app_analytics').insert({ 'user_id': currentUser.id, 'source': 'budget_plan', 'metadata': { 'month': '2024-07', 'category': '餐饮', 'amount': 50000 } }); // 保存一笔2024年7月“餐饮”类的实际支出 await supabaseClient.from('app_analytics').insert({ 'user_id': currentUser.id, 'source': 'budget_expense', 'metadata': { 'month': '2024-07', 'category': '餐饮', 'amount': 3800, 'description': '周五部门聚餐' } });

这么做的几个核心好处:

  1. 避免Schema爆炸:每加一个小功能就建新表,长期来看数据库会变得难以维护。用source字段区分,逻辑清晰,扩展时无需频繁执行ALTER TABLE
  2. 节省Supabase资源:Supabase的免费和收费计划对数据库表数量有限制。复用现有表,相当于在配额内做了最大化利用。
  3. 灵活的数据结构metadata作为JSONB字段,可以存储任意结构的数据。今天预算只需要amount,明天如果想加个color标签,直接存进去就行,前端解析处理即可,后端完全不用动。
  4. 统一的查询接口:所有财务相关数据的读写都通过同一张表,简化了数据访问层的代码。

当然,这种模式不适合数据量极大、需要复杂关联查询或强事务保证的场景。但对于我们这种用户个人财务数据量级(每月几十到几百条记录)和查询模式(主要是按用户、月份、来源筛选),它提供了最佳的开发速度和灵活性。

3. 核心功能模块的深度实现解析

3.1 四标签页布局与状态管理策略

UI上我们采用了经典的顶部标签栏(TabBar)加内容区(TabBarView)的布局,四个标签分别是:概览、预算、AI节建议、未来模拟。状态管理是这里的一个小挑战,因为每个标签的数据(概览的KPI、预算的设置与进度、AI建议内容、模拟计算结果)都是独立获取和更新的,而且有些操作(比如在“预算”页调整金额)需要实时反映在“概览”页的进度条上。

我们没有引入复杂的状态管理库(如Bloc、Riverpod),因为当前模块的复杂度可控。而是使用了Flutter内置的ValueNotifier配合Consumer(来自provider包)来实现局部的、高效的状态响应。具体来说,我们为整个财务页面创建了一个FinancialDataController类,它内部管理着多个ValueNotifier

class FinancialDataController { final ValueNotifier<Map<String, double>> monthlyBudgetNotifier = ValueNotifier({}); final ValueNotifier<List<ExpenseRecord>> currentMonthExpensesNotifier = ValueNotifier([]); final ValueNotifier<String?> aiAdviceNotifier = ValueNotifier(null); final ValueNotifier<double?> simulationResultNotifier = ValueNotifier(null); // 加载预算数据的方法 Future<void> loadBudget(String month) async { final data = await _fetchBudgetFromSupabase(month); monthlyBudgetNotifier.value = data; // 更新Notifier,所有监听它的Widget会自动重建 } // 更新单项预算的方法 Future<void> updateBudget(String category, double newAmount) async { await _saveBudgetToSupabase(category, newAmount); // 先更新本地内存中的数据 final newMap = Map<String, double>.from(monthlyBudgetNotifier.value); newMap[category] = newAmount; monthlyBudgetNotifier.value = newMap; // 触发UI更新 // 同时,概览页的进度条Widget监听了这个Notifier,也会自动更新 } }

在UI中,对于只关心预算数据的Widget,我们用ValueListenableBuilder包裹,这样只有当monthlyBudgetNotifier变化时,这个Widget才会重建,性能最优。这种“细粒度响应式”的模式,在Flutter Web这种单页面应用里,能有效避免不必要的全局重建,保持界面流畅。

3.2 AI节建议生成:低成本接入大语言模型

这是本项目的亮点之一。我们并没有为这个功能单独开发一个新的后端API或Edge Function,而是巧妙地复用了项目中已有的一个通用AI助手函数ai-assistant

实现步骤:

  1. 数据准备:在Flutter前端,我们将用户指定月份(如“2024-07”)的财务数据汇总并格式化成一段清晰的文本。这包括总收入、总支出、以及分门别类的支出明细(例如:“餐饮: ¥85,000,交通: ¥25,000,娱乐: ¥18,000 ...”)。

  2. 构建提示词(Prompt):这是让AI输出高质量建议的关键。我们设计了一个结构化的提示词:

    请扮演一位专业的个人理财顾问。请分析以下用户[2024-07]月份的家计数据,并提供三条具体、可立即行动的节建议。 数据概览: - 总收入:¥450,000 - 总支出:¥380,000 - 主要支出类别: 餐饮:¥85,000 (占支出22.4%) 交通:¥25,000 娱乐:¥18,000 ...(其他类别) 要求: 1. 请基于上述数据,指出最有可能节省开支的1-2个类别。 2. 针对这些类别,提出三条非常具体、实操性强的建议(例如:“尝试每周自带午餐3次,预计每月可节省约¥12,000”,而非“减少餐饮支出”)。 3. 每条建议请用一句话说明,预估每月可节省的金额范围。 4. 输出格式严格遵循:仅输出三条建议,每条以‘• ’开头,使用中文。

    这个提示词明确了AI的角色、输入数据的结构、输出要求(三条、具体、带金额预估)和格式。通过限制输出条数和格式,我们能得到稳定、整洁、可直接在UI上展示的结果,无需复杂的后处理。

  3. 调用Edge Function:通过Supabase客户端库,调用ai-assistant函数,将上述提示词作为消息体发送。

    Future<String> fetchAiAdvice(String month, FinancialSummary summary) async { final prompt = _buildAdvicePrompt(month, summary); // 构建上述提示词 try { final response = await supabase.functions.invoke('ai-assistant', body: { 'action': 'chat', 'message': prompt, }); return response.data['reply'] as String; // 假设返回结构为 {“reply”: “...”} } catch (e) { // 处理网络或API错误,返回友好提示 return 'AI分析暂时不可用,请稍后重试。'; } }
  4. 前端展示:将返回的文本(三条带的建议)用Text组件渲染,或者进一步用正则表达式拆分后放入ListView中,提升视觉效果。

避坑心得:

  • 提示词工程是关键:最初的版本只是简单地把数据扔给AI,结果它可能回复一段冗长的分析文章,或者建议数量不固定。通过精确的提示词约束,才能得到产品化所需的结构化输出。
  • 错误处理必须友好:AI API调用可能因为网络、额度、内容策略等原因失败。前端一定要做好try-catch,给用户明确的反馈(如“分析中...”、“服务繁忙”),而不是让界面卡死或崩溃。
  • 成本控制:复用现有Edge Function,避免了新函数的冷启动开销和额外的监控负担。同时,在提示词中限制输出长度,也能有效控制每次调用消耗的Token数,从而控制成本。

3.3 未来资产模拟:纯Dart实现的复利计算器

“未来模拟”标签页的核心是一个复利计算器。用户输入初始金额、每月追加投资额、预期年化回报率和投资年限,点击计算后,就能看到期末的总资产预估。这个功能完全在前端用Dart实现,不依赖任何后端服务或复杂库。

核心算法实现:我们采用按月复利计算的方式,更贴近大多数基金定投的实际情景。核心函数如下:

/// 计算复利终值(按月计算) /// [principal] 初始本金 /// [monthlyAddition] 每月追加金额 /// [annualRate] 预期年化收益率(百分比,如5.0表示5%) /// [years] 投资年数 double calculateCompoundInterest( double principal, double monthlyAddition, double annualRate, int years) { // 1. 将年利率转换为月利率(小数形式) double monthlyRate = annualRate / 100 / 12; int totalMonths = years * 12; double futureValue = principal; // 2. 按月循环计算 for (int i = 0; i < totalMonths; i++) { // 每月先计算利息:上月本金 * 月利率 // 然后加上本月追加的投资额 futureValue = futureValue * (1 + monthlyRate) + monthlyAddition; } // 3. 返回最终结果 return futureValue; }

为什么选择循环计算而非公式?标准的复利终值公式是FV = P*(1+r)^n + PMT*[((1+r)^n - 1)/r]。虽然公式更高效,但对于大多数用户来说,理解“每月投入、按月复利”这个过程,循环计算在概念上更直观。而且,对于几十年的计算(最多几百次循环),在浏览器的JavaScript/Dart引擎上性能开销完全可以忽略不计,代码的可读性和可维护性收益更大。

一个生动的例子:假设用户有100万日元初始资金,计划每月追加投资3万日元,预期年化回报率为5%,投资20年。

  • 总投入本金 = 1,000,000 + (30,000 * 12 * 20) = 8,200,000日元。
  • 通过上述函数计算,20年后的资产总额约为15,440,000日元。
  • 利息收益部分约为7,240,000日元。这个数字直观地展示了“时间+复利”的威力:利息收益几乎接近本金总额。我们在UI上特意将这个“利息部分”高亮显示,对用户是非常有力的储蓄激励。

UI交互细节:我们使用了TextFormField来接收用户输入,并为其添加了输入验证(确保是正数、利率合理等)。当任何输入框的值发生变化时,我们使用onChanged回调来触发重新计算,并实时更新显示结果,给用户即时的反馈。同时,我们预设了几个“快速设置”按钮(如“保守型3%”、“进取型7%”),方便用户快速切换场景进行对比。

3.4 预算管理与进度可视化

预算页面允许用户在15个预设的生活类别(如住房、餐饮、交通、娱乐、学习等)中设置月度预算。数据通过前面提到的通用表模式保存到Supabase。

可视化实现:每个预算条目都是一个ListTile,包含类别图标、名称、预算金额输入框和一个线性进度条(LinearProgressIndicator)。进度条的长度根据“实际支出 / 预算金额”的比例动态计算。

LinearProgressIndicator( value: expenseAmount / budgetAmount, // 比例,超过1.0则显示为满格(可考虑颜色变红) backgroundColor: Colors.grey[200], valueColor: AlwaysStoppedAnimation<Color>( (expenseAmount / budgetAmount) <= 1.0 ? Colors.blue : Colors.red, ), )

当用户在概览页记录一笔新支出时,该类别对应的进度条会实时更新。这个“实时性”得益于我们之前提到的ValueNotifier状态管理。支出记录保存后,会触发currentMonthExpensesNotifier更新,而预算页的Widget监听相关数据,会自动重绘进度条。

注意事项:

  • 数据一致性:预算和支出都按“年月”(如‘2024-07’)严格区分。查询时务必带上时间范围,避免把上月的支出算到本月。
  • 进度条超限处理:当支出超过预算(比例>1.0)时,我们把进度条颜色设为红色,并且值固定为1.0(填满),这样既能直观告警,又不会让进度条“溢出”UI组件。

4. Flutter Web开发与代码质量维护的实战要点

4.1 保持flutter analyze 0警告的纪律

在团队协作和长期维护中,保持代码静态分析零警告至关重要。这次重构我特别关注了Flutter 3.19(当前稳定版)中analysis_options.yamlrequire_trailing_commas这条规则。它要求在多行的集合字面量、函数调用参数列表的每一行末尾都加上逗号。

为什么这个规则重要?

  1. 版本控制友好:当你在集合中添加一个新元素时,只需要新增一行,上一行的末尾因为已有逗号,所以这行修改在git diff中只会显示为“添加了一行”,而不是“修改了上一行(添加逗号)+ 新增一行”。这让代码审查更清晰。
  2. 格式统一:自动格式化工具(如dart format)能更好地工作,代码风格完全一致。

错误示例和正确示例:

// ❌ 错误:最后一行参数后面缺少逗号,flutter analyze会报错 Widget _buildKpiCard(String title, double value, Color bgColor, Color textColor, IconData icon) { return Card( color: bgColor, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Icon(icon, color: textColor), Text(title, style: TextStyle(color: textColor)), Text(formatCurrency(value), style: TextStyle(...)), ], // <- children 列表的 ] 前面也应该有逗号,但这里先关注参数 ), ), ); } // ✅ 正确:所有多行参数列表、集合的末尾都有逗号 Widget _buildKpiCard( String title, double value, Color bgColor, Color textColor, IconData icon, // <- 参数列表最后一项也有逗号 ) { return Card( color: bgColor, child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Icon(icon, color: textColor), Text(title, style: TextStyle(color: textColor)), Text(formatCurrency(value), style: TextStyle(...)), ], // <- children 列表的 ] 前面也有逗号 ), ), ); }

养成这个习惯后,代码会整洁很多。建议在IDE(VSCode或Android Studio)中配置保存时自动运行dart format,并定期在终端运行flutter analyze,确保团队代码规范。

4.2 适配Flutter版本:DropdownButtonFormField的变迁

另一个在实际开发中遇到的细节是DropdownButtonFormField的API变化。在Flutter 3.3之后,直接设置value属性来预选值的方式被标记为弃用(deprecated),转而推荐使用initialValue

旧方式(已弃用):

String _selectedCategory = '餐饮'; DropdownButtonFormField<String>( value: _selectedCategory, // 在Flutter 3.3+会提示deprecated items: categories.map((String category) { return DropdownMenuItem(value: category, child: Text(category)); }).toList(), onChanged: (newValue) { setState(() { _selectedCategory = newValue!; }); }, );

新方式(推荐):

final _categoryController = TextEditingController(text: '餐饮'); // 通过Controller设置初始值 DropdownButtonFormField<String>( // 不再使用value属性 items: categories.map((String category) { return DropdownMenuItem(value: category, child: Text(category)); }).toList(), onChanged: (newValue) { setState(() { _categoryController.text = newValue!; }); }, controller: _categoryController, // 使用controller // 或者,如果与Form关联,可以使用initialValue // initialValue: '餐饮', );

这个改动是为了更好地将下拉菜单集成到Flutter的Form生态中,使其行为与其他表单字段(如TextFormField)一致。如果你在升级Flutter版本后遇到相关警告,按照新方式修改即可。

4.3 性能优化:列表渲染与数据分页

当支出记录越来越多时,直接在ListView中渲染所有条目可能会导致滚动卡顿。我们采用了ListView.builder来按需构建子项,这是Flutter处理长列表的标准做法。更进一步,如果数据量巨大(虽然家计数据通常不会),可以考虑集成Supabase的实时分页查询。

基础优化示例:

ValueListenableBuilder<List<ExpenseRecord>>( valueListenable: financialController.currentMonthExpensesNotifier, builder: (context, expenses, child) { if (expenses.isEmpty) return _buildEmptyState(); return ListView.builder( itemCount: expenses.length, itemBuilder: (context, index) { final expense = expenses[index]; return ExpenseListItem(expense: expense); // 使用独立的StatelessWidget }, ); }, )

将列表项抽离成独立的StatelessWidget(如ExpenseListItem),可以最小化重绘范围。当只有某一条目的数据变化时,只有那个对应的ListItem会重建,而不是整个列表。

5. 部署、测试与未来迭代方向

5.1 Flutter Web的构建与部署

开发完成后,使用flutter build web命令生成优化的发布包。我们选择部署到Firebase Hosting,因为它与Flutter工具链集成良好,部署简单快捷。

# 1. 构建生产版本 flutter build web --release --web-renderer canvaskit # 使用CanvasKit渲染器以获得更好的浏览器兼容性 # 2. 部署到Firebase (需先安装并登录Firebase CLI) firebase deploy --only hosting

--web-renderer canvaskit是一个重要选项。CanvasKit渲染器能确保UI在不同浏览器中具有最高的一致性,特别是对于自定义图形和文本渲染。虽然初始加载体积会比html渲染器稍大,但对于我们这种包含自定义图表和复杂布局的应用来说,稳定性优先。

5.2 核心功能测试策略

对于这样一个工具,测试重点在于逻辑正确性和用户体验。

  1. 复利计算单元测试:为calculateCompoundInterest函数编写Dart单元测试,验证常见场景(零本金、零利率、长期投资)下的计算结果是否正确,特别是与已知的财务计算器结果进行对比。
  2. AI提示词与解析测试:模拟不同的财务数据输入,检查生成的提示词是否符合预期格式,并模拟Edge Function返回各种格式的文本(包括可能出现的错误信息),测试前端解析和显示逻辑的健壮性。
  3. UI交互测试:使用flutter_test进行Widget测试,模拟用户点击标签页、输入预算、点击计算按钮等操作,验证界面状态是否正确更新。
  4. 集成测试(关键):编写一个简单的集成测试,模拟用户从登录到查看AI建议的完整流程。这能确保前端与Supabase Auth、Database、Functions的集成是可靠的。

5.3 可能的未来扩展方向

这个家计AI顾问模块已经具备了核心功能,但还有很大的深化空间:

  1. 数据可视化增强:引入charts_flutter库,在概览页增加月度收支趋势折线图、支出类别占比饼图,让数据更直观。
  2. AI能力深化
    • 消费预测:基于历史数据,让AI预测下个月在各类别的大致支出。
    • 个性化建议:不仅分析月度数据,还能结合用户的长期目标(如“两年内存够100万日元旅行基金”),给出阶段性的储蓄和支出调整建议。
    • 收据图像识别:通过Supabase Storage上传收据图片,利用Edge Function调用OCR和AI服务,自动提取金额、类别、商家信息,实现“拍照记账”。
  3. 多账户与家庭共享:扩展数据模型,支持用户管理多个账户(如个人账户、家庭共同账户),并实现家庭成员间的预算共享和支出可见(在隐私授权前提下)。
  4. 与日历/待办事项集成:这是我们“AI生活管理应用”的终极愿景。例如,识别到日历中有“朋友生日”事件,AI可以提前一周给出合理的礼物预算建议;或者当某类别支出快超预算时,在待办事项中生成一条“本周减少外出就餐”的提醒。

从128行的静态页面到如今功能丰富的AI家计顾问,这次重构让我深刻体会到,利用好现有的强大工具链(Flutter、Supabase),复用已有能力(AI Edge Function),并专注于解决用户真实痛点(清晰的预算、可操作的节建议、可视化的未来激励),完全可以在短时间内打造出体验出色且功能扎实的产品模块。整个过程中,保持代码的整洁和可维护性,是为未来迭代铺平道路的关键。

http://www.rkmt.cn/news/1416420.html

相关文章:

  • Windows 10终极清理优化指南:如何使用Windows10Debloater快速移除臃肿软件
  • Atlas OS完整指南:三步打造更快速、更隐私的Windows系统
  • 移动机器人底盘运动学模型全解析
  • 基于LM358与NTC热敏电阻的简易温度报警器设计与实现
  • 如何10分钟完成《重返未来:1999》终极自动化助手M9A的专业配置
  • Arduino蓝牙遥控车制作指南:从硬件选型到代码调试全解析
  • 南通外贸建站推荐,WaiMaoYa 外贸鸭一站式全包服务,零基础也能做好外贸建站 - 外贸独立站运营
  • 电路设计入门:从欧姆定律到PCB实战,手把手教你制作自动小夜灯
  • 深圳5家正规雅思培训机构实测排行 基于核心教学指标 - 互联网科技品牌测评
  • 把RouterOS 7.x塞进VMware:不止是安装,更是打造你的第一个软路由实验平台
  • Sora 2 3D空间一致性失效的7种典型崩溃场景(含OpenUSD兼容性故障日志与热修复补丁)
  • Teachable Machine:零代码AI训练神器,让每个人都能成为机器学习创造者
  • 基于Arduino与麦克纳姆轮的蓝牙跟随机器人全栈开发实战
  • 国家中小学智慧教育平台电子课本下载工具:快速获取教材PDF的完整指南
  • 技术美术视角:为什么说Niagara是Cascade的‘超级进化版’?不只是GPU粒子
  • 不只是金融!用甲骨文Crystal Ball做产品可靠性分析:以弹簧材料选择为例
  • 9个用于测试自动化的最佳AI测试工具
  • 为什么你的Veo预览总比渲染慢3帧?——基于NVIDIA CUDA Graph与Veo Pipeline深度剖析(附12组Benchmark数据)
  • 艾德克斯负载电源控制代码(C#)
  • 从UEFI固件到操作系统:深入理解SMBIOS协议在系统启动时的数据流
  • 深圳5家雅思培训机构实测排行:聚焦提分与服务 - 互联网科技品牌测评
  • Windows平台终极解决方案:自动化ADB与Fastboot驱动安装工具的完整技术栈深度解析
  • OpenClaw+88api保姆级教程:国内直连 Claude/GPT 模型,一篇搞定环境配置(2026实测可用)
  • 从QPSK到MSK:一张图看懂相位连续性的演进与频谱优化
  • 执业医师考试哪个课程好?2026这套完整备考方案值得关注 - 医考机构品牌测评专家
  • 从树莓派选系统说起:Raspbian、Ubuntu Server、Debian,新手到底该刷哪个镜像?
  • 【macOS保姆级】Claude Code从安装到API配置全流程:国内直连无需海外账号,亲测跑通
  • 告别黑屏和拉伸!保姆级教程:在Ubuntu上为老旧或特殊显示器自定义分辨率
  • 别再为SAP销售订单批导报错头疼了!详解定价类型(A/B/C/G)选择与条件类型更新逻辑
  • 如何快速掌握浏览器资源捕获:猫抓(cat-catch)专业工具完整实战指南