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 1Base 2Height。生成 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),

矩形字段是 LengthWidth,平行四边形字段是 BaseHeight。公式表达不同,但计算形式都是两个数相乘。

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());
  });
}

这个方法的流程很短:

  1. 根据 _selectedShape 取当前图形。
  2. 读取当前图形的 calculate 函数。
  3. 把所有控制器文本转成列表传入。
  4. 使用 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 模块顺序

页面从上到下依次是:

  1. 图形选择区。
  2. 当前图形公式与输入区。
  3. 面积结果区。

这个顺序符合用户心智:先选图形,再输入尺寸,最后看结果。

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 运行验证要点

  1. 应用可以正常启动到首页。
  2. 六个图形都能在横向列表中访问。
  3. 切换图形后输入框数量和标签正确变化。
  4. 输入数字后面积结果实时刷新。
  5. 清空输入时页面不崩溃。
  6. 小屏下公式标签和结果数字不明显溢出。

鸿蒙适配不是只看能否构建成功,还要看输入法、字体、滚动、触控反馈和多屏尺寸下的表现是否稳定。

十六、性能与可维护性

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 新增图形步骤

基于当前结构,新增图形只需要三步:

  1. _shapes 中添加一条 Map。
  2. 填写 nameiconfieldsformula
  3. 实现对应的 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,这种写法非常适合小工具、计算器、表单生成器和教学示例类应用。

从鸿蒙适配角度看,项目没有复杂原生插件,核心风险不在公式计算,而在输入法、横向滚动、图标字体、公式文本和结果卡片布局。只要这些细节验证稳定,就能获得比较可靠的跨端体验。

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


相关资源:

Logo

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

更多推荐