Flutter 实战:loan_calculator 贷款计算器的等额本息公式、实时重算与鸿蒙适配解析

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

loan_calculator 是一个基于 Flutter 实现的贷款计算器。它支持输入贷款本金、年利率和贷款年限,实时计算月供、总还款和总利息,并在页面底部展示一个简化的前 5 年还款表。

本文基于项目真实源码展开,重点分析 TextEditingController 输入管理等额本息月供公式手写幂运算实时重算机制结果卡片布局简化还款表渲染鸿蒙适配关注点。文章内容可直接发布到 CSDN,不包含面向作者的检查说明。

贷款计算器的核心不是把几个输入框摆出来,而是让输入、公式、结果展示和异常输入处理形成稳定闭环。loan_calculator 很适合作为 Flutter 表单计算类应用的实战样例。

在这里插入图片描述

图示说明:本文围绕 Flutter 贷款计算器的输入、等额本息公式、结果展示和跨端适配展开,适合用于鸿蒙、Android、iOS 等多端工具应用开发复盘。

一、项目定位与功能概览

1.1 应用主题

loan_calculator 的定位是一个 贷款月供与总成本计算工具。用户输入贷款本金、年利率和贷款年限,页面会实时计算贷款结果。

核心功能如下:

功能 页面表现 源码实现
本金输入 Loan Amount 输入框 _principalController
年利率输入 Annual Interest Rate 输入框 _rateController
年限输入 Loan Term 输入框 _yearsController
月供展示 顶部绿色结果卡片 _monthlyPayment
总还款展示 蓝色统计卡片 _totalPayment
总利息展示 橙色统计卡片 _totalInterest
实时重算 输入变化即重新计算 onChanged: (_) => _calculate()
简化还款表 前 5 年展示 _buildAmortizationRow()

1.2 默认输入

项目启动时带有默认输入:

输入项 默认值
Loan Amount 100000
Annual Interest Rate 5
Loan Term 20

页面初始化后会自动执行一次计算,让用户一打开应用就能看到结果。

1.3 当前实现边界

这篇文章需要明确源码边界:当前项目实现的是等额本息月供估算和简化还款表,不是完整金融系统。

能力 当前是否实现 说明
月供计算 等额本息公式
总还款 月供乘总期数
总利息 总还款减本金
输入实时重算 三个输入框都监听变化
精确还款明细 底部表格是简化展示
税费保险 未涉及
浮动利率 未涉及

二、工程结构与运行方式

2.1 工程结构

项目保持标准 Flutter 工程结构,核心代码集中在 lib/main.dart

文件或目录 作用 说明
lib/main.dart 应用入口与页面实现 包含输入、计算和 UI
pubspec.yaml 依赖声明 使用 Flutter SDK 与 Material 图标
test/widget_test.dart Widget 测试入口 可扩展为贷款计算测试
ohos/ 鸿蒙平台工程目录 用于跨端构建和适配

2.2 依赖声明

项目没有引入复杂第三方依赖:

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8

这说明公式计算、输入管理和结果展示全部由 Flutter 与 Dart 完成。

2.3 常用命令

开发和验证时可以使用以下命令:

flutter pub get
flutter analyze
flutter test
flutter run
命令 作用 使用场景
flutter pub get 获取依赖 首次运行或依赖变化
flutter analyze 静态分析 检查语法和 lint
flutter test 执行测试 验证 Widget 行为
flutter run 启动应用 本地调试界面

三、应用入口与主题配置

3.1 main 函数

应用入口保持 Flutter 标准写法:

void main() {
  runApp(const MyApp());
}

贷款计算器不需要启动时加载远程配置,因此入口很简洁。

3.2 MyApp 根组件

根组件负责创建 MaterialApp

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Loan Calculator',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
      ),
      home: const MyHomePage(title: 'Loan Calculator'),
    );
  }
}

这里有三个关键信息:

  • 应用标题是 Loan Calculator
  • 主题种子色是 Colors.green
  • 首页是 MyHomePage

3.3 主题色选择

绿色常用于财务、增长和金额相关界面。源码中月供结果卡片使用绿色,和贷款计算场景比较契合。

当前源码没有显式设置 useMaterial3: true,因此文章以真实代码为准。如果后续需要统一 Material 3 表现,可以在 ThemeData 中补充该配置。

四、状态字段设计

4.1 输入控制器

贷款计算器有三个输入项:

final TextEditingController _principalController = TextEditingController(text: '100000');
final TextEditingController _rateController = TextEditingController(text: '5');
final TextEditingController _yearsController = TextEditingController(text: '20');

4.2 计算结果字段

结果状态有三个:

double _monthlyPayment = 0;
double _totalPayment = 0;
double _totalInterest = 0;

4.3 字段说明

字段 类型 作用
_principalController TextEditingController 管理贷款本金输入
_rateController TextEditingController 管理年利率输入
_yearsController TextEditingController 管理贷款年限输入
_monthlyPayment double 月供
_totalPayment double 总还款
_totalInterest double 总利息

4.4 状态分层

状态类别 字段
输入状态 三个 Controller
计算状态 月供、总还款、总利息
展示状态 结果卡片和还款表

五、Controller 生命周期管理

5.1 初始化默认输入

输入控制器直接带默认值:

TextEditingController(text: '100000')

这样页面打开时输入框已有示例数据,不需要用户从空白开始。

5.2 initState 自动计算

页面初始化后执行计算:


void initState() {
  super.initState();
  _calculate();
}

5.3 dispose 释放资源

页面销毁时释放三个控制器:


void dispose() {
  _principalController.dispose();
  _rateController.dispose();
  _yearsController.dispose();
  super.dispose();
}

5.4 生命周期表

阶段 行为
页面创建 初始化输入控制器
initState 计算默认结果
用户输入 实时重算
页面销毁 释放控制器

六、输入解析与异常处理

6.1 calculate 方法入口

计算逻辑先解析输入:

final principal = double.tryParse(_principalController.text) ?? 0;
final annualRate = double.tryParse(_rateController.text) ?? 0;
final years = int.tryParse(_yearsController.text) ?? 0;

6.2 tryParse 的价值

tryParse 可以避免用户输入非法字符时抛出异常。解析失败时回落为 0。

6.3 非法输入处理

如果本金、年限或年利率不合法,结果全部清零:

if (principal <= 0 || years <= 0 || annualRate <= 0) {
  setState(() {
    _monthlyPayment = 0;
    _totalPayment = 0;
    _totalInterest = 0;
  });
  return;
}

6.4 输入处理表

输入情况 页面结果
本金小于等于 0 结果归零
年限小于等于 0 结果归零
年利率小于等于 0 结果归零
输入无法解析 按 0 处理
三项均有效 执行公式计算

七、等额本息公式实现

7.1 月利率与期数

源码先计算月利率和还款期数:

final monthlyRate = annualRate / 100 / 12;
final numberOfPayments = years * 12;

7.2 月供公式

等额本息月供公式在代码中写为:

final payment = principal *
    (monthlyRate * _pow(1 + monthlyRate, numberOfPayments)) /
    (_pow(1 + monthlyRate, numberOfPayments) - 1);

7.3 公式拆解

变量 含义
principal 贷款本金
monthlyRate 月利率
numberOfPayments 还款总月数
payment 每月还款金额

7.4 总还款和总利息

final total = payment * numberOfPayments;

setState(() {
  _monthlyPayment = payment;
  _totalPayment = total;
  _totalInterest = total - principal;
});

总利息等于总还款减本金。

八、手写幂运算

8.1 pow 方法

源码没有引入 dart:math,而是手写 _pow()

double _pow(double base, int exponent) {
  double result = 1;
  for (int i = 0; i < exponent; i++) {
    result *= base;
  }
  return result;
}

8.2 方法含义

这个方法用于计算:

(1 + monthlyRate) ^ numberOfPayments

8.3 为什么可行

贷款年限一般不会特别大,years * 12 的循环次数可控,因此这种写法在演示项目中可以接受。

8.4 后续优化

如果要提升通用性,可以使用 dart:mathpow,但当前项目保持了零额外导入。

九、实时重算机制

9.1 输入变化触发计算

三个输入框都绑定了 onChanged

onChanged: (_) => _calculate(),

9.2 实时反馈的价值

用户修改本金、利率或年限后,顶部月供和底部统计会立即更新,不需要额外点击按钮。

9.3 实时重算链路

输入变化
  -> onChanged
  -> _calculate()
  -> 解析输入
  -> 执行公式
  -> setState
  -> UI 刷新

9.4 可优化点

对于当前小项目,实时计算性能足够。如果以后计算更复杂,可以增加防抖,避免每输入一个字符就立即计算。

十、顶部月供结果卡片

10.1 结果卡片结构

顶部卡片展示月供:

Card(
  color: Colors.green.shade50,
  child: Padding(
    padding: const EdgeInsets.all(24),
    child: Column(
      children: [
        const Icon(Icons.account_balance, size: 48, color: Colors.green),
        Text('\$${_monthlyPayment.toStringAsFixed(2)}'),
        const Text('Monthly Payment'),
      ],
    ),
  ),
)

10.2 金额格式化

金额使用两位小数:

_monthlyPayment.toStringAsFixed(2)

10.3 视觉层级

元素 作用
银行图标 强化贷款场景
大字号金额 突出月供
Monthly Payment 解释指标含义

10.4 为什么月供放顶部

贷款计算器最核心的问题通常是“每个月要还多少”,因此月供放在最上方是合理的。

十一、贷款输入表单

11.1 Loan Details 卡片

输入表单放在一张 Card 中:

Card(
  child: Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text('Loan Details'),
        // 三个 TextField
      ],
    ),
  ),
)

11.2 本金输入框

TextField(
  controller: _principalController,
  keyboardType: TextInputType.number,
  decoration: InputDecoration(
    labelText: 'Loan Amount',
    prefixText: '\$ ',
  ),
)

11.3 利率输入框

TextField(
  controller: _rateController,
  keyboardType: TextInputType.number,
  decoration: InputDecoration(
    labelText: 'Annual Interest Rate',
    suffixText: '%',
  ),
)

11.4 年限输入框

TextField(
  controller: _yearsController,
  keyboardType: TextInputType.number,
  decoration: InputDecoration(
    labelText: 'Loan Term',
    suffixText: 'years',
  ),
)

十二、总还款与总利息卡片

12.1 双卡片布局

总还款和总利息放在一行:

Row(
  children: [
    Expanded(child: Card(...)),
    const SizedBox(width: 8),
    Expanded(child: Card(...)),
  ],
)

12.2 总还款卡片

蓝色卡片展示总还款:

Text('\$${_totalPayment.toStringAsFixed(2)}')

12.3 总利息卡片

橙色卡片展示总利息:

Text('\$${_totalInterest.toStringAsFixed(2)}')

12.4 指标对比

指标 含义
Total Payment 整个贷款周期总还款
Total Interest 整个贷款周期利息成本

十三、简化还款表渲染

13.1 Amortization Schedule 区域

底部展示还款表:

const Text('Amortization Schedule')
_buildAmortizationRow('Year', 'Principal', 'Interest', 'Balance')

13.2 前 5 年生成

源码只生成前 5 行:

...List.generate(5, (index) {
  final year = index + 1;
  final years = int.tryParse(_yearsController.text) ?? 20;
  if (year > years) return const SizedBox();
  ...
})

13.3 表格行方法

Widget _buildAmortizationRow(String year, String principal, String interest, String balance) {
  return Container(
    padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
    child: Row(
      children: [
        Expanded(child: Text(year)),
        Expanded(child: Text(principal, textAlign: TextAlign.right)),
        Expanded(child: Text(interest, textAlign: TextAlign.right)),
        Expanded(child: Text(balance, textAlign: TextAlign.right)),
      ],
    ),
  );
}

13.4 重要边界说明

当前还款表是简化展示,不是严格的等额本息逐月摊销表。源码中的本金、利息和余额按简化公式估算,主要用于 UI 演示。

十四、鸿蒙适配关注点

14.1 为什么适配风险较低

loan_calculator 主要由 Flutter 标准组件和 Dart 公式计算构成,不依赖网络、数据库、定位、相机或平台通道,因此基础适配风险较低。

模块 是否依赖平台能力 适配关注度
公式计算 Dart 逻辑
数字输入 Flutter 标准输入
结果展示 Flutter 标准组件
表格布局 Flutter Row/Expanded
持久化 当前未实现

14.2 数字键盘验证

鸿蒙设备上需要重点验证:

  • 数字键盘是否正常弹出。
  • 小数点是否可输入。
  • 空输入时是否回落为 0。
  • 输入变化后结果是否立即刷新。

14.3 小屏布局

页面使用 SingleChildScrollView,对小屏比较友好。仍需验证金额过大时是否出现文本溢出。

14.4 金融工具边界

当前项目适合做贷款估算和 Flutter 表单计算演示,不应作为金融决策依据。正式金融产品还需要考虑利率类型、费用、税费、还款方式和合规说明。

十五、测试设计与默认测试改造

15.1 当前测试入口

项目中的测试文件仍是默认计数器测试。对于贷款计算器,更有价值的是验证默认结果、输入变化和非法输入。

15.2 初始页面测试

testWidgets('loan calculator renders initial state', (WidgetTester tester) async {
  await tester.pumpWidget(const MyApp());

  expect(find.text('Loan Calculator'), findsWidgets);
  expect(find.text('Monthly Payment'), findsOneWidget);
  expect(find.text('Total Payment'), findsOneWidget);
  expect(find.text('Total Interest'), findsOneWidget);
});

15.3 输入变化测试

testWidgets('updates when principal changes', (WidgetTester tester) async {
  await tester.pumpWidget(const MyApp());

  await tester.enterText(find.byType(TextField).first, '200000');
  await tester.pump();

  expect(find.text('Loan Amount'), findsOneWidget);
});

15.4 非法输入测试

testWidgets('shows zero when input is invalid', (WidgetTester tester) async {
  await tester.pumpWidget(const MyApp());

  await tester.enterText(find.byType(TextField).first, '0');
  await tester.pump();

  expect(find.text('\$0.00'), findsWidgets);
});

15.5 公式单元测试建议

如果把贷款公式抽成纯函数,就能脱离 Widget 测试公式正确性。

十六、可维护性优化方向

16.1 抽离计算函数

可以把公式抽成纯函数:

double calculateMonthlyPayment({
  required double principal,
  required double annualRate,
  required int years,
}) {
  final monthlyRate = annualRate / 100 / 12;
  final numberOfPayments = years * 12;
  return principal * monthlyRate;
}

完整公式可以保留在业务层,这样测试更方便。

16.2 引入计算结果模型

class LoanResult {
  const LoanResult({
    required this.monthlyPayment,
    required this.totalPayment,
    required this.totalInterest,
  });

  final double monthlyPayment;
  final double totalPayment;
  final double totalInterest;
}

16.3 精确还款计划

如果要做真正摊销表,需要按月循环计算每期利息、本金和余额,而不是按年简化估算。

16.4 增加还款方式

后续可以支持:

  • 等额本息。
  • 等额本金。
  • 只还利息。
  • 提前还款模拟。

十七、功能扩展方向

17.1 增加图表

可以加入本金和利息占比图,让用户直观看到贷款成本。

17.2 增加保存方案

可以保存多个贷款方案,方便对比不同利率和年限。

17.3 增加币种设置

当前固定使用美元符号。后续可以支持人民币、欧元等币种展示。

17.4 增加导出

可以把还款计划导出为 CSV 或 PDF,方便归档。

十八、常见问题与优化建议

18.1 为什么利率为 0 时结果归零

源码把 annualRate <= 0 视为非法输入,直接将结果清零。若要支持无息贷款,需要单独处理利率为 0 的公式。

18.2 为什么要手写幂运算

项目没有导入 dart:math,用 _pow() 完成幂计算。对于演示项目足够清楚。

18.3 为什么输入变化会立即计算

三个输入框都绑定了 onChanged: (_) => _calculate(),所以每次输入变化都会刷新结果。

18.4 为什么还款表不是完整明细

源码只生成前 5 年的简化展示,并没有逐月计算真实摊销。文章中不能把它描述为完整还款计划。

18.5 鸿蒙适配最应该关注什么

重点关注数字输入、小数点、软键盘遮挡、金额显示、表格列宽和长数字溢出。当前项目没有本地保存能力。

十九、完整流程复盘

19.1 页面启动流程

main()
  -> runApp(MyApp)
  -> MaterialApp
  -> MyHomePage
  -> initState
  -> _calculate 默认值
  -> build 渲染结果

19.2 输入变化流程

修改本金/利率/年限
  -> onChanged
  -> _calculate
  -> tryParse
  -> 校验输入
  -> 执行月供公式
  -> setState 更新结果

19.3 展示流程

计算出 monthlyPayment
  -> 顶部卡片显示月供
  -> 双卡片显示总还款和总利息
  -> 底部展示简化还款表

19.4 异常输入流程

输入非法值
  -> tryParse 返回 null
  -> 回落为 0
  -> 校验失败
  -> 结果全部归零

二十、相关资源与继续学习

20.1 Flutter 学习资源

贷款计算器涉及表单、数字输入、计算和布局,可以结合以下资源学习:

资源 内容
Flutter Docs Flutter 官方开发文档
Dart 官方文档 Dart 语言与核心库
Widget catalog Flutter 常用组件
Flutter testing Widget 测试与交互模拟

20.2 贷款工具扩展方向

后续可以继续增强:

  • 等额本金。
  • 完整月度还款表。
  • 贷款方案对比。
  • 图表展示。
  • 多币种。
  • 导出报表。
  • 本地保存。

20.3 跨端实践价值

loan_calculator 很适合作为 Flutter 适配鸿蒙的小型表单计算样例。它依赖很轻,但覆盖了数字输入、实时计算、金额展示、滚动布局和表格行渲染,能帮助开发者验证很多跨端基础能力。

总结

loan_calculator 用简洁的 Flutter 代码实现了贷款月供计算器。它通过三个 TextEditingController 管理本金、年利率和年限输入,通过等额本息公式计算月供,通过总还款和总利息卡片展示贷款成本,并用简化还款表提供额外参考。

从工程角度看,这个项目最值得学习的是“表单输入 + 公式计算 + 实时结果展示”的组合方式。面向鸿蒙适配时,项目依赖较轻,主要需要验证数字输入、软键盘、金额显示和表格布局。对于想学习 Flutter 表单计算类应用的开发者来说,它是一个清晰、实用且容易扩展的案例。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐