Flutter 实战:area_calculator 多图形面积计算器的动态表单、公式模型与鸿蒙适配解析
Flutter 实战:area_calculator 多图形面积计算器的动态表单、公式模型与鸿蒙适配解析
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
面积计算器是一个很适合观察 Flutter 状态设计的小项目。它不像复杂业务系统那样依赖大量接口,却完整覆盖了 数据模型驱动 UI、动态输入框生成、实时公式计算、横向图形选择、结果卡片展示 和 跨端适配验证 等关键环节。
area_calculator 当前支持六类图形:矩形、圆形、三角形、正方形、梯形和平行四边形。每个图形都在同一份数据列表中声明名称、图标、输入字段、公式文本和计算函数,页面根据当前选中的图形动态生成输入框,并在用户输入时实时刷新面积结果。
小工具应用的代码量通常不大,但它很考验结构意识:公式、输入字段和展示文案如果不能统一管理,后续扩展一个图形就会变成到处改代码。
图示说明:上图展示 Flutter 页面在移动端的布局组织方式。area_calculator 的核心界面由图形选择区、公式输入卡片和面积结果卡片组成。
一、项目定位与功能边界
1.1 应用定位
area_calculator 是一个多图形面积计算工具,适用于几何学习、工程估算、日常面积换算和 Flutter 入门项目拆解。它的重点不是复杂算法,而是把多个图形的计算规则组织成可复用的数据结构。
项目当前支持的图形包括:
- Rectangle:矩形面积。
- Circle:圆形面积。
- Triangle:三角形面积。
- Square:正方形面积。
- Trapezoid:梯形面积。
- Parallelogram:平行四边形面积。
1.2 功能模块
| 功能模块 | 页面表现 | 源码实现 |
|---|---|---|
| 图形切换 | 横向滚动图形卡片 | ListView.builder |
| 图形数据 | 名称、图标、字段、公式、计算函数 | _shapes 列表 |
| 动态表单 | 根据字段数量生成输入框 | List.generate(fields.length) |
| 实时计算 | 输入变化后刷新面积 | onChanged 调用 _calculate() |
| 结果展示 | 大字号面积数值 | Card + toStringAsFixed(2) |
| 资源释放 | 页面销毁时释放控制器 | dispose() 遍历释放 |
1.3 技术栈
| 技术点 | 使用位置 | 价值 |
|---|---|---|
| Flutter | 页面、输入框、卡片、图标 | 快速构建跨端 UI |
| Dart | 数据列表、闭包函数、状态逻辑 | 让公式和字段绑定在一起 |
| Material 3 | 主题与组件风格 | useMaterial3: true |
| StatefulWidget | 输入、切换、结果状态 | 适合实时计算工具 |
| TextEditingController | 管理输入框文本 | 支持多个字段复用 |
二、工程结构与运行环境
2.1 目录结构
area_calculator 是标准 Flutter 工程,核心代码集中在 lib/main.dart。
| 文件或目录 | 作用 |
|---|---|
lib/main.dart |
应用入口、图形数据、计算逻辑和 UI 构建 |
pubspec.yaml |
Flutter SDK、图标和测试依赖声明 |
test/widget_test.dart |
Widget 测试入口 |
ohos/ |
鸿蒙平台工程目录 |
analysis_options.yaml |
Dart 静态分析规则 |
2.2 运行命令
flutter doctor
flutter pub get
flutter run
这三个命令分别用于检查环境、恢复依赖和启动应用。当前项目没有引入复杂三方插件,因此运行链路比较轻。
2.3 依赖声明
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
从依赖结构看,项目主体依赖 Flutter SDK 自身能力。面积公式、输入解析和结果展示都在 Dart 层完成,这对鸿蒙侧验证比较友好。
三、应用入口与主题配置
3.1 main 函数
Flutter 应用从 main() 进入,然后加载根组件。
import 'package:flutter/material.dart';
void main() {
runApp(const AreaCalculatorApp());
}
入口函数只做启动动作,不夹杂业务逻辑,整体非常清爽。
3.2 根组件
class AreaCalculatorApp extends StatelessWidget {
const AreaCalculatorApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Area Calculator',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
useMaterial3: true,
),
home: const AreaCalculatorHomePage(title: 'Area Calculator'),
);
}
}
根组件使用 StatelessWidget,因为应用级标题和主题不会随着用户输入变化。真正会变化的图形选择、输入值和计算结果都交给首页 State 管理。
3.3 主题色选择
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange)
橙色主题贯穿图形选中态、公式标签和结果区域。对计算器类工具来说,统一色彩能帮助用户快速识别当前操作焦点。
四、StatefulWidget 与核心状态
4.1 首页组件
class AreaCalculatorHomePage extends StatefulWidget {
const AreaCalculatorHomePage({super.key, required this.title});
final String title;
State<AreaCalculatorHomePage> createState() =>
_AreaCalculatorHomePageState();
}
首页使用 StatefulWidget,因为它需要响应图形切换、输入变化和计算结果刷新。
4.2 状态字段
class _AreaCalculatorHomePageState extends State<AreaCalculatorHomePage> {
int _selectedShape = 0;
final List<TextEditingController> _controllers =
List.generate(4, (_) => TextEditingController());
double _result = 0;
}
| 字段 | 类型 | 作用 |
|---|---|---|
_selectedShape |
int |
当前选中的图形下标 |
_controllers |
List<TextEditingController> |
输入框控制器列表 |
_result |
double |
当前面积计算结果 |
_shapes |
List<Map<String, dynamic>> |
图形元数据与计算函数 |
4.3 控制器数量
当前项目生成了 4 个输入控制器:
final List<TextEditingController> _controllers =
List.generate(4, (_) => TextEditingController());
已支持图形中最多需要 3 个输入字段,例如梯形需要 Base 1、Base 2 和 Height。生成 4 个控制器为后续扩展预留了余量。
五、图形数据模型设计
5.1 _shapes 列表
项目最关键的设计是 _shapes。每个图形都用一个 Map 描述。
final List<Map<String, dynamic>> _shapes = [
{
'name': 'Rectangle',
'icon': Icons.rectangle,
'fields': ['Length', 'Width'],
'formula': 'A = L × W',
'calculate': (c) =>
(double.tryParse(c[0]) ?? 0) * (double.tryParse(c[1]) ?? 0),
},
];
这种结构把 UI 信息和计算规则放在一起,让新增图形的路径非常清晰。
5.2 字段含义
| Key | 类型 | 作用 |
|---|---|---|
name |
String |
图形名称 |
icon |
IconData |
图形选择区使用的图标 |
fields |
List<String> |
输入框标签列表 |
formula |
String |
页面展示的公式 |
calculate |
Function |
面积计算函数 |
5.3 数据驱动 UI 的优势
数据驱动的好处是:页面不需要为每个图形写一套输入表单,而是根据 fields 自动生成输入框,根据 calculate 自动计算面积。
选择图形
读取当前 shape
生成输入字段
用户输入数值
调用 shape.calculate
刷新结果卡片
当一类功能的结构相似、字段不同、算法不同,用数据模型承载差异通常比复制多个 Widget 更耐用。
六、六种面积公式拆解
6.1 公式总览
| 图形 | 输入字段 | 公式 | 计算含义 |
|---|---|---|---|
| Rectangle | Length、Width | A = L × W |
长乘宽 |
| Circle | Radius | A = π × r² |
圆周率乘半径平方 |
| Triangle | Base、Height | A = ½ × b × h |
底乘高再除以 2 |
| Square | Side | A = s² |
边长平方 |
| Trapezoid | Base 1、Base 2、Height | A = ½ × (b1 + b2) × h |
上下底之和乘高再除以 2 |
| Parallelogram | Base、Height | A = b × h |
底乘高 |
6.2 矩形与平行四边形
矩形和平行四边形都使用两个输入字段,公式结构相似。
'calculate': (c) =>
(double.tryParse(c[0]) ?? 0) * (double.tryParse(c[1]) ?? 0),
矩形字段是 Length 和 Width,平行四边形字段是 Base 和 Height。公式表达不同,但计算形式都是两个数相乘。
6.3 圆形
圆形只需要一个半径输入。
'calculate': (c) =>
3.14159 *
(double.tryParse(c[0]) ?? 0) *
(double.tryParse(c[0]) ?? 0),
源码使用 3.14159 作为 π 的近似值,对轻量面积计算器已经足够。如果面向高精度场景,可以考虑使用 dart:math 中的 pi。
6.4 三角形和梯形
三角形使用底和高:
'calculate': (c) =>
0.5 * (double.tryParse(c[0]) ?? 0) * (double.tryParse(c[1]) ?? 0),
梯形使用上底、下底和高:
'calculate': (c) =>
0.5 *
((double.tryParse(c[0]) ?? 0) + (double.tryParse(c[1]) ?? 0)) *
(double.tryParse(c[2]) ?? 0),
这两个公式都使用 0.5 表达二分之一,代码可读性很好。
七、实时计算逻辑
7.1 _calculate 方法
void _calculate() {
final shape = _shapes[_selectedShape];
final calculate = shape['calculate'] as Function;
setState(() {
_result = calculate(_controllers.map((c) => c.text).toList());
});
}
这个方法的流程很短:
- 根据
_selectedShape取当前图形。 - 读取当前图形的
calculate函数。 - 把所有控制器文本转成列表传入。
- 使用
setState()写入面积结果。
7.2 输入解析
每个公式内部都使用 double.tryParse。
double.tryParse(c[0]) ?? 0
这样可以避免用户清空输入框或输入非数字时导致异常。解析失败时按 0 计算,页面仍然稳定。
7.3 结果刷新
输入框通过 onChanged 触发计算。
TextField(
controller: _controllers[index],
keyboardType: TextInputType.number,
onChanged: (_) => _calculate(),
)
用户每次输入都会立刻刷新结果卡片,不需要额外点击按钮。
八、图形选择区实现
8.1 横向 ListView
图形数量为 6 个,适合用横向列表展示。
SizedBox(
height: 80,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _shapes.length,
itemBuilder: (context, index) {
final s = _shapes[index];
final isSelected = _selectedShape == index;
return GestureDetector(
onTap: () {
setState(() {
_selectedShape = index;
for (var c in _controllers) {
c.clear();
}
_result = 0;
});
},
child: Container(),
);
},
),
)
横向滚动能在小屏上容纳更多图形,同时不会压缩主表单空间。
8.2 切换图形后的状态重置
setState(() {
_selectedShape = index;
for (var c in _controllers) {
c.clear();
}
_result = 0;
});
切换图形时清空输入框和结果,可以避免上一种图形的输入值误用于下一种图形。
8.3 选中态样式
| 状态 | 背景色 | 图标颜色 | 文本颜色 |
|---|---|---|---|
| 已选中 | 橙色 | 白色 | 白色 |
| 未选中 | 浅灰色 | 灰色 | 灰色 |
清晰的选中态能让用户知道当前公式属于哪个图形。
九、动态输入表单生成
9.1 当前图形字段
在 build() 方法中,先读取当前图形和字段列表:
final shape = _shapes[_selectedShape];
final fields = shape['fields'] as List<String>;
fields 决定页面需要生成几个输入框。
9.2 List.generate 生成输入框
...List.generate(fields.length, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: TextField(
controller: _controllers[index],
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: fields[index],
suffixText: 'units',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
onChanged: (_) => _calculate(),
),
);
})
这段代码是动态表单的核心。矩形会生成两个输入框,圆形生成一个输入框,梯形生成三个输入框。
9.3 动态表单对扩展的价值
如果后续新增椭圆,只需要增加一条图形数据:
{
'name': 'Ellipse',
'icon': Icons.circle,
'fields': ['Major Radius', 'Minor Radius'],
'formula': 'A = π × a × b',
'calculate': (c) =>
3.14159 *
(double.tryParse(c[0]) ?? 0) *
(double.tryParse(c[1]) ?? 0),
}
页面会自动生成对应输入框并调用计算函数,不需要重写 UI 主体。
十、公式展示与结果卡片
10.1 公式标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(
shape['formula'] as String,
style: TextStyle(
fontFamily: 'monospace',
color: Colors.orange.shade800,
),
),
)
公式标签让用户在输入前先理解当前计算逻辑,尤其适合学习类和教育类工具。
10.2 结果展示
Text(
_result.toStringAsFixed(2),
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Colors.orange,
),
)
结果保留两位小数,可以让圆形、三角形、梯形这类结果更稳定地展示。
10.3 单位表达
结果下方固定显示:
const Text('square units')
输入框使用 units 作为通用长度单位,结果使用 square units 表达面积单位。这样可以避免绑定米、厘米、英尺等具体单位,让工具保持通用。
十一、页面布局结构
11.1 Scaffold 骨架
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 图形选择区、输入卡片、结果卡片
],
),
),
);
SingleChildScrollView 让页面在小屏、横屏或键盘弹出时仍能滚动。
11.2 模块顺序
页面从上到下依次是:
- 图形选择区。
- 当前图形公式与输入区。
- 面积结果区。
这个顺序符合用户心智:先选图形,再输入尺寸,最后看结果。
11.3 组件职责
| 组件 | 职责 |
|---|---|
ListView.builder |
根据 _shapes 构建图形按钮 |
Card |
承载公式和输入框 |
TextField |
收集长度、宽度、半径等输入 |
Container |
控制颜色、圆角和渐变 |
Text |
展示公式、标签和结果 |
十二、生命周期与资源释放
12.1 dispose 方法
void dispose() {
for (var c in _controllers) {
c.dispose();
}
super.dispose();
}
TextEditingController 持有输入状态和监听能力,页面销毁时应主动释放。
12.2 为什么要遍历释放
当前项目使用控制器列表,而不是单个控制器。因此释放时需要遍历:
for (var c in _controllers) {
c.dispose();
}
这种写法和生成控制器时的 List.generate 对应,结构上很清楚。
12.3 生命周期路径
创建首页 State
生成输入控制器
用户切换图形并输入尺寸
实时计算并刷新结果
页面销毁
释放全部控制器
这条路径完整体现了 Flutter 表单类页面的基本生命周期。
十三、异常输入与边界场景
13.1 空输入处理
double.tryParse('') ?? 0
空字符串无法解析为数字,因此会回退为 0。页面不会崩溃,结果也可预测。
13.2 非数字输入处理
如果用户输入非数字字符,double.tryParse 同样返回 null,公式按 0 处理。
| 输入内容 | 解析结果 | 面积影响 |
|---|---|---|
10 |
10.0 |
正常计算 |
2.5 |
2.5 |
正常计算 |
| 空字符串 | 0 |
结果趋向 0 |
| 非数字 | 0 |
结果趋向 0 |
13.3 负数输入
当前源码没有显式禁止负数。由于面积通常不应为负,后续可以在输入层限制正数,或在计算函数中使用绝对值、校验提示等方式处理。
13.4 图形切换边界
切换图形时,源码会清空所有输入并把结果归零。这能防止输入字段数量变化后出现旧值污染。
for (var c in _controllers) {
c.clear();
}
_result = 0;
十四、Widget 测试设计
14.1 基础渲染测试
import 'package:flutter_test/flutter_test.dart';
import '../lib/main.dart';
void main() {
testWidgets('area calculator renders home page', (tester) async {
await tester.pumpWidget(const AreaCalculatorApp());
expect(find.text('Area Calculator'), findsWidgets);
expect(find.text('Rectangle'), findsOneWidget);
expect(find.text('Area'), findsOneWidget);
});
}
这个测试覆盖根组件、默认图形和结果区域。
14.2 矩形输入测试
testWidgets('rectangle area updates after input', (tester) async {
await tester.pumpWidget(const AreaCalculatorApp());
await tester.enterText(find.byType(TextField).at(0), '10');
await tester.enterText(find.byType(TextField).at(1), '5');
await tester.pump();
expect(find.text('50.00'), findsOneWidget);
});
矩形是默认图形,适合用作最基础的实时计算测试。
14.3 图形切换测试
testWidgets('circle mode shows radius field', (tester) async {
await tester.pumpWidget(const AreaCalculatorApp());
await tester.tap(find.text('Circle'));
await tester.pump();
expect(find.text('Radius'), findsOneWidget);
expect(find.text('A = π × r²'), findsOneWidget);
});
这个测试验证 _selectedShape 变化后,字段和公式是否同步更新。
14.4 测试命令
flutter test
保持测试入口中的根组件名称与真实源码一致,可以避免默认模板测试遗留造成编译失败。
十五、鸿蒙适配观察
15.1 适配优势
area_calculator 主要由 Flutter Widget 和 Dart 公式组成,没有复杂原生插件依赖,因此鸿蒙适配重点相对集中。
| 维度 | 当前项目情况 | 鸿蒙侧关注点 |
|---|---|---|
| 公式计算 | Dart 闭包函数 | 多端结果一致 |
| 输入控件 | TextField |
数字键盘、小数点、负号 |
| 图形选择 | 横向 ListView |
滚动手势和触控反馈 |
| 图标资源 | Material Icons | 图标字体渲染 |
| 结果展示 | Card 和大字号文本 |
小屏宽度和字体缩放 |
15.2 构建命令参考
flutter clean
flutter pub get
flutter build hap
具体命令取决于使用的鸿蒙 Flutter 适配环境。对这个项目来说,主要验证页面渲染、输入、滚动和结果计算即可。
15.3 运行验证要点
- 应用可以正常启动到首页。
- 六个图形都能在横向列表中访问。
- 切换图形后输入框数量和标签正确变化。
- 输入数字后面积结果实时刷新。
- 清空输入时页面不崩溃。
- 小屏下公式标签和结果数字不明显溢出。
鸿蒙适配不是只看能否构建成功,还要看输入法、字体、滚动、触控反馈和多屏尺寸下的表现是否稳定。
十六、性能与可维护性
16.1 性能特征
面积计算属于轻量 CPU 任务。页面性能主要取决于 Widget 构建是否简洁、状态刷新是否集中、横向列表是否稳定。
| 维度 | 当前表现 |
|---|---|
| 计算量 | 很小 |
| 状态数量 | 少 |
| 输入框数量 | 最多按字段动态生成 |
| 列表规模 | 6 个图形 |
| 重绘范围 | 页面级 setState |
16.2 当前结构优点
_shapes把图形差异集中管理。_calculate()统一处理公式调用。- 输入框按字段动态生成,避免重复 UI。
- 切换图形时清空状态,交互语义清晰。
- 控制器统一创建和释放,生命周期明确。
16.3 可以继续演进的方向
如果项目后续变大,可以引入更明确的模型类。
class ShapeFormula {
const ShapeFormula({
required this.name,
required this.fields,
required this.formula,
required this.calculate,
});
final String name;
final List<String> fields;
final String formula;
final double Function(List<String> values) calculate;
}
模型类能替代 Map<String, dynamic>,减少运行期类型转换,让 IDE 提示更准确。
十七、扩展一个新图形的思路
17.1 新增图形步骤
基于当前结构,新增图形只需要三步:
- 在
_shapes中添加一条 Map。 - 填写
name、icon、fields、formula。 - 实现对应的
calculate函数。
17.2 示例:新增椭圆
{
'name': 'Ellipse',
'icon': Icons.circle_outlined,
'fields': ['Major Radius', 'Minor Radius'],
'formula': 'A = π × a × b',
'calculate': (c) =>
3.14159 *
(double.tryParse(c[0]) ?? 0) *
(double.tryParse(c[1]) ?? 0),
}
页面会自动出现新的图形选项,并生成两个输入框。
17.3 扩展时的注意点
| 注意点 | 说明 |
|---|---|
| 字段数量 | 不要超过控制器列表可用数量 |
| 图标选择 | Material 图标需要在目标端正常显示 |
| 公式文本 | 应和计算函数保持一致 |
| 单位表达 | 输入是长度单位,输出是面积单位 |
| 测试覆盖 | 至少覆盖一个新增图形的输入和结果 |
十八、常见问题与优化建议
18.1 为什么使用 Map 而不是多个 if 分支
因为六种图形的页面结构高度相似,差异主要在字段和公式。用 Map 可以把差异集中管理,避免在 UI 中写大量分支。
18.2 为什么切换图形要清空输入
不同图形需要的字段数量不同。如果不清空输入,矩形的长宽可能会被圆形或梯形继续使用,造成用户误解。
18.3 为什么结果保留两位小数
圆形和梯形结果常常不是整数。两位小数能让结果更稳定,也能避免小数位过长影响布局。
18.4 为什么输入框统一显示 units
项目没有绑定具体单位,所以使用 units 表示通用长度单位。只要输入单位一致,输出就是对应的平方单位。
18.5 为什么当前结果可能出现负数
源码没有限制负数输入。如果输入负数,公式会按数学表达计算。面积类产品通常可以增加正数校验,让结果更符合业务语义。
18.6 为什么可以迁移到鸿蒙侧验证
项目依赖简单,核心公式运行在 Dart 层,不依赖平台通道。鸿蒙侧主要验证 Flutter 渲染、输入法、图标字体、滚动和窗口适配。
总结
area_calculator 用一份 _shapes 数据列表完成了多图形面积计算器的核心设计:每个图形声明自己的名称、图标、输入字段、公式文本和计算函数,页面则根据当前图形动态生成输入框,并在用户输入时实时刷新结果。
从 Flutter 实战角度看,这个项目的亮点是结构清楚、状态集中、扩展路径明确。它没有为每种图形复制一套页面,而是通过数据驱动方式复用同一套 UI,这种写法非常适合小工具、计算器、表单生成器和教学示例类应用。
从鸿蒙适配角度看,项目没有复杂原生插件,核心风险不在公式计算,而在输入法、横向滚动、图标字体、公式文本和结果卡片布局。只要这些细节验证稳定,就能获得比较可靠的跨端体验。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐


所有评论(0)