Flutter三方库适配OpenHarmony【unit_converter】单位转换器项目完整实战
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
unit_converter 是一个基于 Flutter 的单位转换器项目,核心代码位于 lib/main.dart。项目通过结构化模型描述转换分类和单位列表,支持 Length、Weight、Temperature、Volume 四类单位转换,并使用输入框、横向 FilterChip、DropdownButtonFormField、交换按钮和结果卡片组成完整工具界面。
这个项目适合讲解 Flutter 工具类应用在 OpenHarmony 上的适配过程。它覆盖了 结构化数据建模 、输入监听 、普通单位基准换算 、温度特殊公式 、横向分类切换 、下拉选择 、结果格式化 和 Material 3 组件渲染。

图片说明:本文围绕 Flutter 表单组件、单位转换模型和 OpenHarmony 承载工程展开,所有关键代码均来自 unit_converter 的真实源码。
单位转换器的核心不是堆叠更多 UI,而是把"普通单位统一换算到基准单位"和"温度单位使用独立公式"这两个规则拆清楚。
一、项目背景与目标
1.1 项目定位
unit_converter 是一个多分类单位转换工具。用户输入数值,选择分类、来源单位和目标单位后,页面实时展示转换结果。当前项目没有网络请求、数据库或复杂插件依赖,所有换算都在 Flutter 层完成。
当前项目真实支持的功能包括:
- 输入待转换数值。
- 支持 Length 长度分类。
- 支持 Weight 重量分类。
- 支持 Temperature 温度分类。
- 支持 Volume 体积分类。
- 使用
FilterChip横向切换分类。 - 使用
DropdownButtonFormField选择 From 单位。 - 使用
DropdownButtonFormField选择 To 单位。 - 点击交换按钮互换 From 和 To。
- 输入变化时自动重新转换。
- 分类切换时重置单位索引。
- 普通单位通过
toBase换算。 - 温度单位使用 Celsius、Fahrenheit、Kelvin 特殊公式。
- 结果保留 6 位小数,并移除多余尾零。
1.2 技术目标
本文重点拆解以下内容:
- Flutter 应用入口和绿色 Material 3 主题。
ConversionCategory和ConversionUnit的模型设计。- 四类单位数据如何组织。
TextEditingController.addListener如何实现实时转换。- 普通单位如何通过基准单位完成换算。
- 温度单位为什么不能直接使用比例系数换算。
FilterChip如何实现横向分类选择。DropdownButtonFormField如何实现单位选择。- 交换按钮如何互换来源和目标单位。
- OpenHarmony 侧如何验证输入、下拉、分类和结果渲染。
1.3 核心实现速览
| 能力 | 当前实现 | 适配关注点 |
|---|---|---|
| 应用入口 | runApp(const UnitConverterApp()) |
确认首屏启动 |
| 主题 | ColorScheme.fromSeed(seedColor: Colors.green) |
确认绿色主题 |
| 分类模型 | ConversionCategory |
确认分类数据可读 |
| 单位模型 | ConversionUnit |
确认单位名称、符号、系数 |
| 输入监听 | _inputController.addListener(_convert) |
确认输入实时换算 |
| 普通换算 | input * from.toBase / to.toBase |
确认比例单位正确 |
| 温度换算 | _convertTemperature |
确认摄氏、华氏、开尔文公式 |
| 分类切换 | FilterChip |
确认横向滚动与选中态 |
| 单位选择 | DropdownButtonFormField |
确认弹层和选项展示 |
| 结果格式化 | toStringAsFixed(6) + RegExp |
确认尾零处理 |
二、环境准备与工程结构
2.1 工程结构
项目保持 Flutter 标准目录,同时包含 OpenHarmony 平台工程。
| 文件或目录 | 作用 |
|---|---|
lib/main.dart |
应用入口、模型、数据、换算逻辑和 UI |
pubspec.yaml |
SDK 约束、Flutter 依赖和资源配置 |
analysis_options.yaml |
Flutter lint 规则 |
test/ |
Flutter 测试目录 |
ohos/ |
OpenHarmony 平台承载工程 |
README.md |
项目说明文件 |
lib/main.dart 是当前项目的核心文件。模型、单位数据、输入监听、换算公式和页面组件都集中在这个文件中。
2.2 依赖配置
项目使用 Dart SDK ^3.9.2,业务功能依赖 Flutter SDK。
yaml
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
单位转换不需要网络、数据库或额外三方库。所有公式都可以在 Dart 层直接计算。
2.3 常用命令
bash
flutter pub get
flutter analyze
flutter test
flutter run
| 命令 | 用途 |
|---|---|
flutter pub get |
获取依赖 |
flutter analyze |
执行静态分析 |
flutter test |
执行测试 |
flutter run |
运行应用 |
OpenHarmony 适配时,还需要结合本地 Flutter OpenHarmony 工具链完成平台构建和真机运行。
三、应用入口与主题配置
3.1 main 函数
项目入口直接启动根组件。
dart
import 'package:flutter/material.dart';
void main() {
runApp(const UnitConverterApp());
}
只引入 material.dart,说明当前应用依赖的是 Flutter Material 组件体系。
3.2 UnitConverterApp
UnitConverterApp 负责创建 MaterialApp。
dart
class UnitConverterApp extends StatelessWidget {
const UnitConverterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Unit Converter',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
),
home: const UnitConverterHomePage(title: 'Unit Converter'),
);
}
}
这段代码包含三个关键点:
- 应用标题为
Unit Converter。 - 主题种子色为
Colors.green。 - 首页为
UnitConverterHomePage。
3.3 首屏验证
OpenHarmony 上运行时,首屏应看到:
- 顶部 AppBar 标题为
Unit Converter。 - 横向分类 Chip 默认选中 Length。
- 输入框默认值为
1。 - From 默认是 Meter。
- To 默认是 Kilometer。
- 结果卡片显示
0.001 km。
这些首屏元素可以快速验证 Flutter 页面是否被正确承载。
四、数据模型设计
4.1 ConversionCategory
分类模型包含名称、图标和单位列表。
dart
class ConversionCategory {
final String name;
final IconData icon;
final List<ConversionUnit> units;
ConversionCategory({required this.name, required this.icon, required this.units});
}
| 字段 | 类型 | 作用 |
|---|---|---|
name |
String |
分类名称,如 Length |
icon |
IconData |
分类图标 |
units |
List<ConversionUnit> |
当前分类下的单位列表 |
4.2 ConversionUnit
单位模型包含单位名称、符号和基准换算系数。
dart
class ConversionUnit {
final String name;
final String symbol;
final double toBase;
ConversionUnit({required this.name, required this.symbol, required this.toBase});
}
| 字段 | 类型 | 作用 |
|---|---|---|
name |
String |
单位完整名称 |
symbol |
String |
单位符号 |
toBase |
double |
转换到当前分类基准单位的系数 |
4.3 模型设计优点
这种结构的优点是数据和 UI 可以自然联动:
- 分类 Chip 使用
name和icon。 - 下拉菜单使用
name和symbol。 - 普通转换公式使用
toBase。 - 新增单位时只需要扩展数据列表。
工具类应用应优先把可变化的业务数据模型化。模型清晰后,UI 只是模型的展示和选择入口。
五、分类与单位数据
5.1 Length 长度分类
长度分类以 Meter 作为基准单位。
dart
ConversionCategory(
name: 'Length',
icon: Icons.straighten,
units: [
ConversionUnit(name: 'Meter', symbol: 'm', toBase: 1),
ConversionUnit(name: 'Kilometer', symbol: 'km', toBase: 1000),
ConversionUnit(name: 'Centimeter', symbol: 'cm', toBase: 0.01),
ConversionUnit(name: 'Millimeter', symbol: 'mm', toBase: 0.001),
ConversionUnit(name: 'Mile', symbol: 'mi', toBase: 1609.344),
ConversionUnit(name: 'Yard', symbol: 'yd', toBase: 0.9144),
ConversionUnit(name: 'Foot', symbol: 'ft', toBase: 0.3048),
ConversionUnit(name: 'Inch', symbol: 'in', toBase: 0.0254),
],
)
| 单位 | 符号 | toBase |
|---|---|---|
| Meter | m | 1 |
| Kilometer | km | 1000 |
| Centimeter | cm | 0.01 |
| Millimeter | mm | 0.001 |
| Mile | mi | 1609.344 |
| Yard | yd | 0.9144 |
| Foot | ft | 0.3048 |
| Inch | in | 0.0254 |
5.2 Weight 重量分类
重量分类以 Kilogram 作为基准单位。
dart
ConversionCategory(
name: 'Weight',
icon: Icons.fitness_center,
units: [
ConversionUnit(name: 'Kilogram', symbol: 'kg', toBase: 1),
ConversionUnit(name: 'Gram', symbol: 'g', toBase: 0.001),
ConversionUnit(name: 'Milligram', symbol: 'mg', toBase: 0.000001),
ConversionUnit(name: 'Pound', symbol: 'lb', toBase: 0.453592),
ConversionUnit(name: 'Ounce', symbol: 'oz', toBase: 0.0283495),
ConversionUnit(name: 'Ton', symbol: 't', toBase: 1000),
],
)
| 单位 | 符号 | toBase |
|---|---|---|
| Kilogram | kg | 1 |
| Gram | g | 0.001 |
| Milligram | mg | 0.000001 |
| Pound | lb | 0.453592 |
| Ounce | oz | 0.0283495 |
| Ton | t | 1000 |
5.3 Temperature 温度分类
温度分类包含 Celsius、Fahrenheit 和 Kelvin。
dart
ConversionCategory(
name: 'Temperature',
icon: Icons.thermostat,
units: [
ConversionUnit(name: 'Celsius', symbol: 'C', toBase: 1),
ConversionUnit(name: 'Fahrenheit', symbol: 'F', toBase: 1),
ConversionUnit(name: 'Kelvin', symbol: 'K', toBase: 1),
],
)
温度单位虽然也保留了 toBase 字段,但真实转换不使用比例系数,而是走 _convertTemperature。
5.4 Volume 体积分类
体积分类以 Liter 作为基准单位。
dart
ConversionCategory(
name: 'Volume',
icon: Icons.water_drop,
units: [
ConversionUnit(name: 'Liter', symbol: 'L', toBase: 1),
ConversionUnit(name: 'Milliliter', symbol: 'mL', toBase: 0.001),
ConversionUnit(name: 'Gallon', symbol: 'gal', toBase: 3.78541),
ConversionUnit(name: 'Quart', symbol: 'qt', toBase: 0.946353),
ConversionUnit(name: 'Pint', symbol: 'pt', toBase: 0.473176),
ConversionUnit(name: 'Cup', symbol: 'cup', toBase: 0.236588),
],
)
| 单位 | 符号 | toBase |
|---|---|---|
| Liter | L | 1 |
| Milliliter | mL | 0.001 |
| Gallon | gal | 3.78541 |
| Quart | qt | 0.946353 |
| Pint | pt | 0.473176 |
| Cup | cup | 0.236588 |
六、页面状态设计
6.1 状态字段
页面状态集中在 _UnitConverterHomePageState 中。
dart
class _UnitConverterHomePageState extends State<UnitConverterHomePage> {
final TextEditingController _inputController = TextEditingController(text: '1');
int _selectedCategory = 0;
int _fromUnit = 0;
int _toUnit = 1;
double _result = 0;
}
| 字段 | 初始值 | 作用 |
|---|---|---|
_inputController |
1 |
管理输入值 |
_selectedCategory |
0 |
当前分类索引,默认 Length |
_fromUnit |
0 |
来源单位索引 |
_toUnit |
1 |
目标单位索引 |
_result |
0 |
转换结果 |
6.2 生命周期
初始化时,输入控制器添加监听,并立即执行一次转换。
dart
@override
void initState() {
super.initState();
_inputController.addListener(_convert);
_convert();
}
页面销毁时释放输入控制器。
dart
@override
void dispose() {
_inputController.dispose();
super.dispose();
}
6.3 实时转换链路
输入框变化后,TextEditingController 会触发 _convert:
- 用户修改输入值。
_inputController监听器触发。_convert解析输入。- 根据分类选择普通转换或温度转换。
setState更新_result。- 结果卡片刷新。
七、普通单位转换逻辑
7.1 _convert 主流程
_convert 是项目的核心转换方法。
dart
void _convert() {
final input = double.tryParse(_inputController.text) ?? 0;
final category = _categories[_selectedCategory];
double result;
if (_selectedCategory == 2) {
result = _convertTemperature(input, category.units[_fromUnit].name, category.units[_toUnit].name);
} else {
final baseValue = input * category.units[_fromUnit].toBase;
result = baseValue / category.units[_toUnit].toBase;
}
setState(() {
_result = result;
});
}
这段代码先解析输入,再根据分类判断使用哪一种转换方式。
7.2 普通单位公式
普通单位先转换到基准单位,再从基准单位转换到目标单位。
dart
final baseValue = input * category.units[_fromUnit].toBase;
result = baseValue / category.units[_toUnit].toBase;
公式可以写成:
text
result = input * from.toBase / to.toBase
7.3 示例推导
以 1 Meter 转 Kilometer 为例:
text
input = 1
from.toBase = 1
to.toBase = 1000
result = 1 * 1 / 1000 = 0.001
以 1 Mile 转 Meter 为例:
text
input = 1
from.toBase = 1609.344
to.toBase = 1
result = 1 * 1609.344 / 1 = 1609.344
7.4 普通单位适用范围
| 分类 | 是否适用 toBase |
基准单位 |
|---|---|---|
| Length | 适用 | Meter |
| Weight | 适用 | Kilogram |
| Temperature | 不适用 | Celsius 作为中间值 |
| Volume | 适用 | Liter |
普通单位之间是线性比例关系,因此可以使用统一公式。
八、温度转换逻辑
8.1 为什么温度要单独处理
温度转换不是简单比例关系。摄氏度和华氏度之间存在偏移量,开尔文与摄氏度之间也存在 273.15 的偏移,因此不能只通过 toBase 系数完成。
8.2 转换到 Celsius
代码先把来源温度转换为 Celsius。
dart
double _convertTemperature(double value, String from, String to) {
double celsius;
if (from == 'Celsius') {
celsius = value;
} else if (from == 'Fahrenheit') {
celsius = (value - 32) * 5 / 9;
} else {
celsius = value - 273.15;
}
if (to == 'Celsius') {
return celsius;
} else if (to == 'Fahrenheit') {
return celsius * 9 / 5 + 32;
} else {
return celsius + 273.15;
}
}
8.3 从 Celsius 转目标单位
| 目标单位 | 公式 |
|---|---|
| Celsius | celsius |
| Fahrenheit | celsius * 9 / 5 + 32 |
| Kelvin | celsius + 273.15 |
8.4 温度示例
| 输入 | From | To | 结果 |
|---|---|---|---|
| 0 | Celsius | Fahrenheit | 32 |
| 100 | Celsius | Kelvin | 373.15 |
| 32 | Fahrenheit | Celsius | 0 |
| 273.15 | Kelvin | Celsius | 0 |
温度转换的特殊处理是这个项目最重要的工程分支。
九、分类切换 UI
9.1 横向 ListView
分类区域使用固定高度横向列表。
dart
SizedBox(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _categories.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
selected: _selectedCategory == index,
label: Row(
children: [
Icon(_categories[index].icon, size: 18),
const SizedBox(width: 4),
Text(_categories[index].name),
],
),
onSelected: (selected) {
setState(() {
_selectedCategory = index;
_fromUnit = 0;
_toUnit = 1;
});
_convert();
},
),
);
},
),
)
横向滚动适合分类数量较多的工具类页面。当前有 4 个分类,在小屏上也能保持可用。
9.2 FilterChip 状态
FilterChip 的选中态由 _selectedCategory == index 控制。
dart
selected: _selectedCategory == index
切换分类后,代码将 _fromUnit 重置为 0,将 _toUnit 重置为 1,避免旧分类的单位索引越界。
9.3 分类切换影响
| 操作 | 状态变化 | 结果 |
|---|---|---|
| 点击 Length | _selectedCategory = 0 |
使用长度单位 |
| 点击 Weight | _selectedCategory = 1 |
使用重量单位 |
| 点击 Temperature | _selectedCategory = 2 |
使用温度公式 |
| 点击 Volume | _selectedCategory = 3 |
使用体积单位 |
分类切换后立即调用 _convert,结果卡片会同步刷新。
十、输入框与页面滚动
10.1 输入框
输入框使用 TextField。
dart
TextField(
controller: _inputController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Enter value',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
style: const TextStyle(fontSize: 24),
)
keyboardType: TextInputType.number 让移动端优先显示数字键盘。fontSize: 24 让输入值更醒目。
10.2 输入解析
输入解析使用 double.tryParse。
dart
final input = double.tryParse(_inputController.text) ?? 0;
非法输入或空输入会回退到 0,不会抛出异常。
10.3 SingleChildScrollView
页面主体使用滚动容器。
dart
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 分类、输入、单位选择、结果
],
),
)
这对 OpenHarmony 移动设备很重要。软键盘弹出或屏幕高度较小时,内容仍可滚动访问。
十一、单位选择与交换
11.1 From 下拉框
来源单位使用 DropdownButtonFormField<int>。
dart
DropdownButtonFormField<int>(
value: _fromUnit,
decoration: const InputDecoration(labelText: 'From'),
items: category.units.asMap().entries.map((entry) {
return DropdownMenuItem(
value: entry.key,
child: Text('${entry.value.name} (${entry.value.symbol})'),
);
}).toList(),
onChanged: (value) {
setState(() {
_fromUnit = value ?? 0;
});
_convert();
},
)
asMap().entries 可以同时获得单位索引和单位对象,便于将索引作为下拉值。
11.2 To 下拉框
目标单位逻辑与来源单位一致,只是默认索引和状态字段不同。
dart
DropdownButtonFormField<int>(
value: _toUnit,
decoration: const InputDecoration(labelText: 'To'),
items: category.units.asMap().entries.map((entry) {
return DropdownMenuItem(
value: entry.key,
child: Text('${entry.value.name} (${entry.value.symbol})'),
);
}).toList(),
onChanged: (value) {
setState(() {
_toUnit = value ?? 1;
});
_convert();
},
)
11.3 交换按钮
两个下拉框中间有一个交换按钮。
dart
IconButton(
onPressed: () {
setState(() {
final temp = _fromUnit;
_fromUnit = _toUnit;
_toUnit = temp;
});
_convert();
},
icon: const Icon(Icons.swap_horiz),
)
交换逻辑只需要互换两个索引,然后重新计算结果。
11.4 单位选择卡片
下拉和交换按钮放在同一张 Card 中。
dart
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(child: fromDropdown),
swapButton,
Expanded(child: toDropdown),
],
),
),
)
这种结构让 From、Swap、To 三个控件在视觉上属于同一组转换操作。
十二、结果卡片与格式化
12.1 结果卡片
结果区域使用绿色浅色卡片。
dart
Card(
color: Colors.green.shade50,
elevation: 8,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const Text('Result', style: TextStyle(fontSize: 16, color: Colors.grey)),
const SizedBox(height: 8),
Text(
'${_result.toStringAsFixed(6).replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r'\.$'), '')} ${category.units[_toUnit].symbol}',
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
),
)
结果卡片展示数值和目标单位符号,例如 0.001 km。
12.2 保留 6 位小数
代码先使用 toStringAsFixed(6) 固定 6 位小数。
dart
_result.toStringAsFixed(6)
这可以避免浮点数直接输出过长。
12.3 移除尾零
随后使用两个正则去掉多余尾零和末尾小数点。
dart
replaceAll(RegExp(r'0+$'), '').replaceAll(RegExp(r'\.$'), '')
| 原始格式 | 处理后 |
|---|---|
1.000000 |
1 |
0.001000 |
0.001 |
12.340000 |
12.34 |
32.000000 |
32 |
这种格式化适合工具类页面,结果既不会过长,也不会显示无意义尾零。
十三、OpenHarmony 适配要点
13.1 基础组件验证
当前项目使用了大量 Flutter Material 基础组件。
| 组件 | 作用 | OpenHarmony 关注点 |
|---|---|---|
MaterialApp |
应用根组件 | 首屏加载 |
Scaffold |
页面结构 | AppBar 和 Body |
FilterChip |
分类选择 | 选中态和横向滚动 |
TextField |
数值输入 | 键盘、焦点、输入同步 |
DropdownButtonFormField |
单位选择 | 菜单弹层和选项 |
IconButton |
交换单位 | 点击响应 |
Card |
分组和结果展示 | 阴影、圆角、颜色 |
13.2 输入与键盘
OpenHarmony 上需要验证:
- 点击输入框后软键盘能正常弹出。
- 数字输入能同步触发转换。
- 空输入或非法输入不会导致页面异常。
- 软键盘弹出后页面仍能滚动访问下方结果。
13.3 下拉菜单
下拉菜单适配需要确认:
- 点击 From 能展开单位列表。
- 点击 To 能展开单位列表。
- 选中某个单位后菜单关闭。
- 结果立即刷新。
- 长单位名称不会严重溢出。
13.4 分类切换
分类切换时要观察:
- Chip 选中态是否正确。
- 图标和文字是否对齐。
- 切换到 Temperature 后是否使用温度公式。
- 切换分类后 From 和 To 是否回到前两个单位。
- 结果是否根据新分类立即刷新。
OpenHarmony 适配验证要覆盖"输入、选择、交换、计算、展示"完整链路,只看页面能打开是不够的。
十四、测试与验证
14.1 初始页面测试
Widget 测试可以验证初始页面结构。
dart
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('unit converter shows initial widgets', (tester) async {
await tester.pumpWidget(const UnitConverterApp());
expect(find.text('Unit Converter'), findsWidgets);
expect(find.text('Length'), findsOneWidget);
expect(find.text('Enter value'), findsOneWidget);
expect(find.text('From'), findsOneWidget);
expect(find.text('To'), findsOneWidget);
expect(find.text('Result'), findsOneWidget);
});
}
这类测试关注 UI 基础结构,不依赖复杂交互。
14.2 普通单位公式测试
普通单位公式可以抽成纯函数后测试。
dart
double convertByBase(double input, double fromBase, double toBase) {
return input * fromBase / toBase;
}
测试示例:
dart
void main() {
test('meter to kilometer', () {
expect(convertByBase(1, 1, 1000), 0.001);
});
test('kilometer to meter', () {
expect(convertByBase(1, 1000, 1), 1000);
});
}
14.3 温度公式测试
温度转换适合单独测试。
dart
double fahrenheitToCelsius(double value) {
return (value - 32) * 5 / 9;
}
double celsiusToKelvin(double value) {
return value + 273.15;
}
典型断言:
dart
void main() {
test('32F equals 0C', () {
expect(fahrenheitToCelsius(32), 0);
});
test('0C equals 273.15K', () {
expect(celsiusToKelvin(0), 273.15);
});
}
14.4 手工验证矩阵
| 场景 | 操作 | 预期 |
|---|---|---|
| 默认转换 | 打开应用 | 1 m 转为 0.001 km |
| 长度转换 | 选择 Mile 到 Meter | 1 mi 为 1609.344 m |
| 重量转换 | 选择 Kilogram 到 Gram | 1 kg 为 1000 g |
| 温度转换 | 选择 Celsius 到 Fahrenheit | 0 C 为 32 F |
| 体积转换 | 选择 Liter 到 Milliliter | 1 L 为 1000 mL |
| 交换单位 | 点击交换按钮 | From 和 To 互换 |
| 非法输入 | 输入空值或文字 | 结果按 0 处理 |
十五、常见问题与优化建议
15.1 为什么分类切换要重置单位索引
不同分类的单位数量不同。如果不重置 _fromUnit 和 _toUnit,从 Length 切换到 Temperature 时可能保留一个超出范围的索引。
dart
setState(() {
_selectedCategory = index;
_fromUnit = 0;
_toUnit = 1;
});
这能保证新分类至少有两个可用单位时,下拉框状态稳定。
15.2 为什么温度不能用 toBase
长度、重量和体积是线性比例关系。温度存在偏移量,例如 0 摄氏度等于 32 华氏度,并不是乘以一个固定系数。因此温度需要单独公式。
dart
celsius = (value - 32) * 5 / 9;
15.3 为什么输入变化要实时转换
输入控制器监听 _convert 后,用户每次修改数值都会刷新结果。
dart
_inputController.addListener(_convert);
这比点击按钮转换更符合单位工具的使用习惯。
15.4 如何增加更多分类
新增分类只需要添加一个 ConversionCategory。
dart
ConversionCategory(
name: 'Area',
icon: Icons.crop_square,
units: [
ConversionUnit(name: 'Square Meter', symbol: 'm²', toBase: 1),
ConversionUnit(name: 'Square Kilometer', symbol: 'km²', toBase: 1000000),
],
)
由于 UI 基于 _categories 动态渲染,新增分类后 Chip 和下拉菜单会自动更新。
15.5 如何提高输入鲁棒性
当前代码使用 double.tryParse,非法输入回退到 0。更完整的做法可以增加输入过滤器,只允许输入数字、小数点和负号。
dart
TextField(
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
)
这样可以更好地支持小数和负数输入,尤其适合温度转换。
15.6 如何处理结果精度
当前结果最多显示 6 位小数,并移除尾零。
dart
_result.toStringAsFixed(6)
如果用于工程测量或科学计算,可以把精度做成配置项,让用户选择 2 位、4 位、6 位或更多位。
十六、工程扩展方向
16.1 拆分模型文件
当前模型写在 main.dart,后续可以拆到独立文件。
text
lib/
models/
conversion_category.dart
conversion_unit.dart
data/
conversion_categories.dart
utils/
conversion_math.dart
pages/
unit_converter_home_page.dart
拆分后,模型、数据、公式和页面职责更清晰。
16.2 抽取转换函数
可以将转换逻辑抽成纯函数。
dart
double convertUnit({
required double input,
required double fromBase,
required double toBase,
}) {
return input * fromBase / toBase;
}
纯函数更容易写单元测试,也便于在其他页面复用。
16.3 增加最近使用记录
单位转换器可以记录用户最近使用的分类和单位组合。
dart
class RecentConversion {
final String category;
final String fromUnit;
final String toUnit;
RecentConversion({
required this.category,
required this.fromUnit,
required this.toUnit,
});
}
这样用户下次进入应用时可以快速恢复常用转换。
16.4 增加收藏单位组合
如果用户经常进行相同转换,例如 km 到 mi、kg 到 lb,可以提供收藏组合功能。
dart
class FavoriteConversion {
final int categoryIndex;
final int fromUnitIndex;
final int toUnitIndex;
FavoriteConversion(this.categoryIndex, this.fromUnitIndex, this.toUnitIndex);
}
收藏能力可以与本地存储结合,在 OpenHarmony 上提供更完整的工具体验。
十七、相关链接与延伸阅读
17.1 Flutter 官方资料
| 资料 | 链接 |
|---|---|
| Flutter 官方文档 | https://docs.flutter.dev/ |
| Flutter API 文档 | https://api.flutter.dev/ |
| TextField | https://api.flutter.dev/flutter/material/TextField-class.html |
| FilterChip | https://api.flutter.dev/flutter/material/FilterChip-class.html |
| DropdownButtonFormField | https://api.flutter.dev/flutter/material/DropdownButtonFormField-class.html |
| IconButton | https://api.flutter.dev/flutter/material/IconButton-class.html |
| Card | https://api.flutter.dev/flutter/material/Card-class.html |
17.2 Dart 与 OpenHarmony 资料
| 资料 | 链接 |
|---|---|
| Dart 官方文档 | https://dart.dev/ |
| Dart API 文档 | https://api.dart.dev/ |
| RegExp API | https://api.dart.dev/stable/dart-core/RegExp-class.html |
| pub.dev | https://pub.dev/ |
| OpenHarmony docs | https://github.com/openharmony/docs |
| 开源鸿蒙跨平台社区 | https://openharmonycrossplatform.csdn.net |
总结
unit_converter 是一个非常典型的 Flutter 工具类应用。它通过 ConversionCategory 描述分类,通过 ConversionUnit 描述单位,并用 toBase 字段完成 Length、Weight、Volume 这类普通单位的统一换算。对于 Temperature,项目单独使用 _convertTemperature 处理 Celsius、Fahrenheit 和 Kelvin 之间的偏移公式,避免把温度错误地当作线性比例单位处理。
从 UI 角度看,项目使用 FilterChip 做分类切换,使用 TextField 接收输入,使用 DropdownButtonFormField 选择来源和目标单位,使用 IconButton 交换单位,并通过绿色结果卡片展示格式化后的转换结果。从 OpenHarmony 适配角度看,重点应验证输入框、软键盘、横向 Chip、下拉菜单、交换按钮、实时计算和结果格式化是否稳定。
掌握这个项目后,可以继续扩展面积、速度、时间、能量、压力等更多单位分类,也可以抽取纯转换函数、增加测试覆盖、加入最近使用记录和收藏组合,让单位转换器从 Demo 工具逐步演进为更完整的跨平台应用。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源: