Flutter for OpenHarmony 文件转换助手App实战 - 单位转换
单位转换这个功能看起来小,但只要你把结构搭顺了(入口薄、弹窗只管交互、换算规则集中管理),后续扩展单位类型会非常省心.我建议你先把“长度”这条链路打磨顺:输入体验、结果展示、复制、异常输入处理都稳定了,再按相同套路加重量/温度/体积.欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net。

单位转换是日常生活和工作中常见的需求。用户可以快速进行各种单位的转换。本文将详细讲解如何实现单位转换工具。
单位转换工具的应用
单位转换在多个领域都有应用。在日常生活中,用户可能需要转换长度、重量、温度等单位。在工程和科学领域,单位转换是必不可少的。
在工具页面中,单位转换工具已经定义在工具列表中:
{'icon': Icons.straighten, 'title': '单位转换'},
说明
这里我把它当成“工具箱”里的一个入口项来做,和文件转换、编码转换这种工具同级。入口层只负责展示,不要在这里塞任何换算逻辑,后面维护会很痛。
单位转换对话框的实现
实际项目里我一般会把“弹窗 UI”与“换算逻辑”分开:弹窗只做交互与数据流转,换算规则集中放在一个纯 Dart 的工具类里,这样后续要加重量/体积/温度时不会改一堆 UI。
先给工具卡片/列表绑定点击事件。入口方法尽量薄一点:
void _onToolTap(BuildContext context, String title) {
if (title == '单位转换') {
_showUnitConverterDialog(context);
return;
}
_showToolDetail(context, title);
}
说明
这里用 title 做分发只是为了把示例讲清楚;真实项目里我更倾向用枚举或 ToolType,避免字符串写错导致“点了没反应”。
接下来是弹窗内部的状态准备。输入框、结果框都用 TextEditingController 管起来,配合下拉框的当前选择值,随时触发重新计算:
final _inputCtrl = TextEditingController();
final _resultCtrl = TextEditingController();
String _fromUnit = '米';
String _toUnit = '厘米';
说明
_resultCtrl 设置为只读,统一从计算结果写入,避免用户手动改结果造成状态不同步。单位选择用两个字符串就够了,后面扩展到“单位类型”时再加一层分类。
弹窗框架用 showDialog + StatefulBuilder,这样不需要为了一个弹窗专门写 StatefulWidget:
void _showUnitConverterDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('单位转换'),
content: _buildUnitConverterBody(setState),
actions: _buildUnitConverterActions(context),
);
},
);
},
);
}
说明
这种写法的优点是“就地管理弹窗内的状态”,不会污染页面级别的 setState。缺点也明显:弹窗复杂到一定程度,还是建议抽成独立组件,顺便加单测.
下面把内容区域拆成几个小块:输入、单位选择、结果展示。先放输入框,输入变化时立刻触发计算:
Widget _buildInputField(VoidCallback recalc) {
return TextField(
controller: _inputCtrl,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: '输入数值',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
onChanged: (_) => recalc(),
);
}
说明
我习惯把 recalc 传进来,而不是在控件内部直接引用外部方法名,这样后续要接防抖、要改触发时机(比如失焦再算)会更顺手.
单位选择部分也拆开写,避免把一堆 DropdownButtonFormField 堆在同一个方法里:
Widget _buildUnitDropdown({
required String value,
required List<String> items,
required ValueChanged<String?> onChanged,
}) {
return DropdownButtonFormField<String>(
value: value,
items: items
.map((u) => DropdownMenuItem(value: u, child: Text(u)))
.toList(),
onChanged: onChanged,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
);
}
说明
这个小组件是为了“复用+一致样式”。只要边框、圆角统一,弹窗看起来会干净很多,也方便以后把单位列表替换成从配置读取.
然后把两个下拉框排成一行,变化时更新状态并计算:
Widget _buildUnitRow(void Function(void Function()) setState, VoidCallback recalc) {
const units = ['米', '厘米', '毫米', '英尺', '英寸'];
return Row(
children: [
Expanded(
child: _buildUnitDropdown(
value: _fromUnit,
items: units,
onChanged: (v) {
if (v == null) return;
setState(() => _fromUnit = v);
recalc();
},
),
),
SizedBox(width: 8.w),
const Icon(Icons.arrow_forward),
SizedBox(width: 8.w),
Expanded(
child: _buildUnitDropdown(
value: _toUnit,
items: units,
onChanged: (v) {
if (v == null) return;
setState(() => _toUnit = v);
recalc();
},
),
),
],
);
}
说明
这里我没有做“from/to 不能相同”的限制,因为相同单位转换对用户也有意义(等于做一次格式化)。如果你希望交互更强一点,可以在相同单位时自动交换或直接提示.
结果框保持只读,所有结果由计算函数写进去:
Widget _buildResultField() {
return TextField(
controller: _resultCtrl,
readOnly: true,
decoration: InputDecoration(
hintText: '转换结果',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
),
);
}
说明
只读的好处是避免“结果框被编辑后你还得反推状态”。对工具类 App 来说,减少状态分支就是减少 bug.
把内容区串起来,顺便把 recalc 定义在同一个作用域里,保证它能拿到最新的单位选择值:
Widget _buildUnitConverterBody(void Function(void Function()) setState) {
void recalc() {
final raw = _inputCtrl.text.trim();
final value = double.tryParse(raw);
if (value == null) {
_resultCtrl.text = '';
return;
}
final result = UnitConverter.convertLength(value, _fromUnit, _toUnit);
_resultCtrl.text = _formatNumber(result);
}
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildInputField(recalc),
SizedBox(height: 12.h),
_buildUnitRow(setState, recalc),
SizedBox(height: 12.h),
_buildResultField(),
],
),
);
}
说明
这里的处理比较“实用主义”:输入不是数字就清空结果,不弹错、不打断用户。你如果要更严格,可以在输入非法时给一个轻提示(比如 SnackBar),但别每敲一个字就弹一次.
按钮部分保持简单:关闭与复制。复制时从 _resultCtrl.text 取值最稳:
List<Widget> _buildUnitConverterActions(BuildContext context) {
return [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('关闭'),
),
TextButton(
onPressed: () {
final text = _resultCtrl.text.trim();
if (text.isNotEmpty) {
Clipboard.setData(ClipboardData(text: text));
}
Navigator.pop(context);
},
child: const Text('复制'),
),
];
}
说明
复制逻辑最好“只复制结果本身”,不要把单位前后缀硬拼进去,不然用户粘贴到别处还要删。真要带单位的话,可以做一个开关选项.
单位转换的实现
单位转换最核心的点就两个:
- 单位的“基准值”如何定义(统一换算到米、千克这类基础单位)
- 计算结果如何展示(精度、格式化、异常输入处理)
长度这种线性单位最简单:统一换算到“米”作为基准即可.
class UnitConverter {
static const Map<String, double> lengthUnits = {
'米': 1.0,
'厘米': 0.01,
'毫米': 0.001,
'英尺': 0.3048,
'英寸': 0.0254,
};
}
说明
lengthUnits 的值表示“该单位对应多少米”。比如 1 英尺 = 0.3048 米,所以是 0.3048. 这样做的好处是:所有单位互转都能通过“先归一再换算”完成.
真正的转换方法最好不要用 ! 强行断言;实际项目里用户可能会切到别的单位类型,或者单位列表更新了但老缓存还在。更稳妥的写法是先判空:
static double convertLength(double value, String fromUnit, String toUnit) {
final from = lengthUnits[fromUnit];
final to = lengthUnits[toUnit];
if (from == null || to == null) return value;
final baseInMeter = value * from;
return baseInMeter / to;
}
说明
这里我用“乘以 from、除以 to”的写法,是因为 lengthUnits 定义的是“1 单位等于多少米”。你也可以反过来定义成“1 米等于多少单位”,但别混用,混一次就容易算错.
结果展示上,我一般会做一个很轻量的格式化:保留一段合理精度,同时把末尾多余的 0 去掉:
String _formatNumber(double v) {
final s = v.toStringAsPrecision(12);
return s.replaceFirst(RegExp(r'\.0+'), '');
}
说明
UI 上“看起来舒服”比数学上无限精确更重要.toStringAsPrecision(12) 通常够用,既不会太长,也能覆盖常见单位换算的误差.
多种单位类型的支持
多单位类型我不建议一上来就全堆在一个 Map 里。更适合的方式是:
- UI 上先让用户选“单位类型”(长度/重量/温度)
- 再根据类型切换不同的单位列表与换算方法
如果你想先快速落地,可以先只做长度,然后按相同方式扩展重量:
static const Map<String, double> weightUnits = {
'千克': 1.0,
'克': 0.001,
'磅': 0.45359237,
};
static double convertWeight(double value, String fromUnit, String toUnit) {
final from = weightUnits[fromUnit];
final to = weightUnits[toUnit];
if (from == null || to == null) return value;
final baseInKg = value * from;
return baseInKg / to;
}
说明
重量和长度一样属于“线性比例换算”,可以完全复用同一套思路。后面你只要把 UI 的 units 列表换成重量单位,并把 convertLength 替换成 convertWeight 即可.
温度转换的特殊处理
温度是个例外:它不是纯比例关系,有“零点偏移”。所以温度不要走系数表,直接写公式更直观:
static double convertTemperature(double value, String from, String to) {
double c;
switch (from) {
case '摄氏度':
c = value;
break;
case '华氏度':
c = (value - 32) * 5 / 9;
break;
case '开尔文':
c = value - 273.15;
break;
default:
c = value;
}
switch (to) {
case '摄氏度':
return c;
case '华氏度':
return c * 9 / 5 + 32;
case '开尔文':
return c + 273.15;
default:
return c;
}
}
说明
我先把输入统一转成摄氏度,再从摄氏度转出去。逻辑虽然啰嗦一点,但非常不容易出错,也方便以后加兰氏度之类的扩展.
精度和舍入
单位转换结果经常会出现很长的小数(尤其英制/公制互转)。我的经验是:
- 默认不要强行固定成 2 位或 4 位
- 先给一个合理精度上限(比如 10-12 位有效数字)
- 再把视觉噪音(末尾的 0)清掉
如果你需要“可配置精度”,可以给对话框加一个简单的设置项,例如 DropdownButton 选择“保留 2/4/6 位”,然后把 _formatNumber 改成 toStringAsFixed(n).
总结
单位转换这个功能看起来小,但只要你把结构搭顺了(入口薄、弹窗只管交互、换算规则集中管理),后续扩展单位类型会非常省心.
我建议你先把“长度”这条链路打磨顺:输入体验、结果展示、复制、异常输入处理都稳定了,再按相同套路加重量/温度/体积.
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)