Flutter实战:打造专业级单位换算器,支持8大类50+单位互转
单位换算是日常生活和工程计算中的高频需求。本文将手把手教你用Flutter构建一款功能完备的单位换算器,涵盖长度、面积、体积、重量、温度、时间、速度、数据存储等8大类别,支持50多种单位的实时互转。
效果预览




这款单位换算器具备以下特点:
- 🎯 8大类别:长度、面积、体积、重量、温度、时间、速度、数据
- 🔄 实时换算:输入即转换,无需点击按钮
- 📊 全量展示:一次显示所有单位的换算结果
- 🌡️ 温度特殊处理:摄氏度、华氏度、开尔文三者互转
- 💫 流畅交互:单位选择器、交换按钮、清除功能
架构设计
逻辑层
视图层
数据层
UnitCategory
UnitType
UnitData
UnitConverterApp
CategorySelector
ConverterPage
InputCard
ConversionCard
AllResultsCard
_convert
通用换算
温度换算
_formatNumber
科学计数法
小数处理
核心数据模型
单位类别与单位类型
单位换算的核心在于建立统一的数据模型。每个类别包含多个单位,每个单位都有一个到基准单位的转换系数。
dart
/// 单位类别
class UnitCategory {
final String name; // 类别名称
final IconData icon; // 图标
final Color color; // 主题色
final List<UnitType> units; // 包含的单位列表
const UnitCategory({
required this.name,
required this.icon,
required this.color,
required this.units,
});
}
/// 单位类型
class UnitType {
final String name; // 单位名称
final String symbol; // 单位符号
final double toBase; // 转换到基准单位的系数
const UnitType({
required this.name,
required this.symbol,
required this.toBase,
});
}
单位数据定义
以长度单位为例,选择"米"作为基准单位(toBase = 1),其他单位的系数表示"1个该单位等于多少米":
| 单位 | 符号 | 转换系数 | 说明 |
|---|---|---|---|
| 千米 | km | 1000 | 1km = 1000m |
| 米 | m | 1 | 基准单位 |
| 厘米 | cm | 0.01 | 1cm = 0.01m |
| 英里 | mi | 1609.344 | 1mi ≈ 1609m |
| 英尺 | ft | 0.3048 | 1ft ≈ 0.3m |
dart
// 长度单位定义
UnitCategory(
name: '长度',
icon: Icons.straighten,
color: Colors.blue,
units: [
UnitType(name: '千米', symbol: 'km', toBase: 1000),
UnitType(name: '米', symbol: 'm', toBase: 1),
UnitType(name: '分米', symbol: 'dm', toBase: 0.1),
UnitType(name: '厘米', symbol: 'cm', toBase: 0.01),
UnitType(name: '毫米', symbol: 'mm', toBase: 0.001),
UnitType(name: '英里', symbol: 'mi', toBase: 1609.344),
UnitType(name: '码', symbol: 'yd', toBase: 0.9144),
UnitType(name: '英尺', symbol: 'ft', toBase: 0.3048),
UnitType(name: '英寸', symbol: 'in', toBase: 0.0254),
UnitType(name: '海里', symbol: 'nmi', toBase: 1852),
],
),
换算算法详解
通用换算公式
对于大多数单位,换算遵循简单的线性关系:
结果=输入值×源单位系数目标单位系数 结果 = 输入值 \times \frac{源单位系数}{目标单位系数} 结果=输入值×目标单位系数源单位系数
换算过程分两步:
- 转为基准单位 :基准值=输入值×源单位系数基准值 = 输入值 \times 源单位系数基准值=输入值×源单位系数
- 转为目标单位 :结果=基准值目标单位系数结果 = \frac{基准值}{目标单位系数}结果=目标单位系数基准值
dart
void _convert() {
final input = double.tryParse(_inputController.text) ?? 0;
final fromUnit = widget.category.units[_fromUnitIndex];
final toUnit = widget.category.units[_toUnitIndex];
double result;
if (widget.category.name == '温度') {
result = _convertTemperature(input, fromUnit.symbol, toUnit.symbol);
} else {
// 通用换算:先转到基准单位,再转到目标单位
final baseValue = input * fromUnit.toBase;
result = baseValue / toUnit.toBase;
}
setState(() {
_result = _formatNumber(result);
_calculateAllResults(input);
});
}
温度换算的特殊处理
温度单位之间不是简单的倍数关系,需要特殊处理。三种温度单位的转换公式:
× 9/5 + 32
(F-32) × 5/9
- 273.15 - 273.15 摄氏度 °C
华氏度 °F
开尔文 K
转换策略:以摄氏度为中转站,任意两个温度单位的转换都先转为摄氏度,再转为目标单位。
°F=°C×95+32K=°C+273.15 \begin{aligned} °F &= °C \times \frac{9}{5} + 32 \\ K &= °C + 273.15 \end{aligned} °FK=°C×59+32=°C+273.15
dart
double _convertTemperature(double value, String from, String to) {
// 第一步:转换为摄氏度
double celsius;
switch (from) {
case '°C':
celsius = value;
break;
case '°F':
celsius = (value - 32) * 5 / 9;
break;
case 'K':
celsius = value - 273.15;
break;
default:
celsius = value;
}
// 第二步:转换为目标单位
switch (to) {
case '°C':
return celsius;
case '°F':
return celsius * 9 / 5 + 32;
case 'K':
return celsius + 273.15;
default:
return celsius;
}
}
数值格式化
换算结果可能非常大或非常小,需要智能格式化以保证可读性:
dart
String _formatNumber(double value) {
if (value == 0) return '0';
// 极大或极小的数使用科学计数法
if (value.abs() >= 1e10 || (value.abs() < 1e-6 && value != 0)) {
return value.toStringAsExponential(6);
}
// 整数直接显示
if (value == value.roundToDouble()) {
return value.toInt().toString();
}
// 小数去除末尾多余的0
String str = value.toStringAsFixed(8);
str = str.replaceAll(RegExp(r'0+$'), '');
str = str.replaceAll(RegExp(r'\.$'), '');
return str;
}
格式化规则:
| 数值范围 | 处理方式 | 示例 |
|---|---|---|
| ∣x∣≥1010|x| \geq 10^{10}∣x∣≥1010 | 科学计数法 | 1.234567e+12 |
| ∣x∣<10−6|x| < 10^{-6}∣x∣<10−6 | 科学计数法 | 1.234567e-8 |
| 整数 | 直接显示 | 1000 |
| 小数 | 去除末尾0 | 3.14 |
UI组件实现
类别选择器
横向滚动的类别选择器,每个类别用图标和名称展示,选中状态有明显的视觉反馈:
dart
Widget _buildCategorySelector() {
return Container(
height: 100,
padding: const EdgeInsets.symmetric(vertical: 8),
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: UnitData.categories.length,
itemBuilder: (context, index) {
final category = UnitData.categories[index];
final isSelected = index == _selectedCategoryIndex;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: InkWell(
onTap: () => setState(() => _selectedCategoryIndex = index),
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 80,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? category.color.withValues(alpha: 0.2)
: Colors.grey.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? category.color : Colors.transparent,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
category.icon,
color: isSelected ? category.color : Colors.grey,
size: 28,
),
const SizedBox(height: 4),
Text(
category.name,
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color: isSelected ? category.color : Colors.grey,
),
),
],
),
),
),
);
},
),
);
}
输入卡片
大字号输入框,配合清除按钮,提供舒适的输入体验:
dart
Widget _buildInputCard(Color color) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'输入数值',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 12),
TextField(
controller: _inputController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: color,
),
decoration: InputDecoration(
hintText: '0',
border: InputBorder.none,
suffixIcon: _inputController.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _inputController.clear(),
)
: null,
),
),
],
),
),
);
}
单位选择器
点击单位区域弹出底部选择器,支持快速切换单位:
dart
void _showUnitPicker(bool isFromUnit) {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isFromUnit ? '选择源单位' : '选择目标单位',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.category.units.length,
itemBuilder: (context, index) {
final unit = widget.category.units[index];
final isSelected = isFromUnit
? index == _fromUnitIndex
: index == _toUnitIndex;
return ListTile(
title: Text('${unit.name} (${unit.symbol})'),
trailing: isSelected
? Icon(Icons.check, color: widget.category.color)
: null,
selected: isSelected,
onTap: () {
setState(() {
if (isFromUnit) {
_fromUnitIndex = index;
} else {
_toUnitIndex = index;
}
_convert();
});
Navigator.pop(context);
},
);
},
),
),
],
),
);
},
);
}
全量结果展示
一次性展示输入值转换为所有其他单位的结果,方便对比:
dart
void _calculateAllResults(double input) {
final fromUnit = widget.category.units[_fromUnitIndex];
_allResults = [];
for (int i = 0; i < widget.category.units.length; i++) {
if (i == _fromUnitIndex) continue; // 跳过源单位
final toUnit = widget.category.units[i];
double result;
if (widget.category.name == '温度') {
result = _convertTemperature(input, fromUnit.symbol, toUnit.symbol);
} else {
final baseValue = input * fromUnit.toBase;
result = baseValue / toUnit.toBase;
}
_allResults.add({
'name': toUnit.name,
'symbol': toUnit.symbol,
'value': _formatNumber(result),
});
}
}
完整单位数据
应用支持的全部8大类别及其单位:
单位换算器
长度
千米/米/分米
厘米/毫米
英里/码/英尺/英寸
海里
面积
平方千米/公顷/亩
平方米/平方分米
平方厘米
平方英里/英亩/平方英尺
体积
立方米/升/毫升
立方厘米
加仑美/加仑英
品脱/盎司液
重量
吨/千克/克/毫克
磅/盎司
斤/两
温度
摄氏度
华氏度
开尔文
时间
年/月/周/天
小时/分钟/秒
毫秒
速度
米每秒/千米每时
英里每时/节
马赫/光速
数据
TB/GB/MB
KB/Byte/bit
数据存储单位说明
数据存储单位采用二进制标准(1KB = 1024B),而非十进制标准(1KB = 1000B):
| 单位 | 字节数 | 计算方式 |
|---|---|---|
| TB | 2402^{40}240 | 1,099,511,627,776 B |
| GB | 2302^{30}230 | 1,073,741,824 B |
| MB | 2202^{20}220 | 1,048,576 B |
| KB | 2102^{10}210 | 1,024 B |
| Byte | 202^020 | 1 B |
| bit | 2−32^{-3}2−3 | 0.125 B |
dart
UnitCategory(
name: '数据',
icon: Icons.storage,
color: Colors.pink,
units: [
UnitType(name: 'TB', symbol: 'TB', toBase: 1099511627776),
UnitType(name: 'GB', symbol: 'GB', toBase: 1073741824),
UnitType(name: 'MB', symbol: 'MB', toBase: 1048576),
UnitType(name: 'KB', symbol: 'KB', toBase: 1024),
UnitType(name: 'Byte', symbol: 'B', toBase: 1),
UnitType(name: 'bit', symbol: 'bit', toBase: 0.125),
],
),
状态管理流程
视图 _calculateAllResults() _formatNumber() _convert() 输入框 用户 视图 _calculateAllResults() _formatNumber() _convert() 输入框 用户 alt [温度单位] [其他单位] 输入数值 TextEditingController监听 判断是否温度单位 _convertTemperature() 基准值换算 格式化结果 计算全部结果 格式化每个结果 setState() 更新显示
扩展建议
如果想进一步完善这个换算器,可以考虑:
- 历史记录:保存最近的换算记录,方便回顾
- 收藏功能:收藏常用的单位组合
- 货币换算:接入汇率API,支持实时货币转换
- 自定义单位:允许用户添加自定义单位
- 语音输入:支持语音输入数值
项目结构
lib/
└── main.dart # 完整应用代码
├── UnitCategory # 单位类别模型
├── UnitType # 单位类型模型
├── UnitData # 单位数据定义
├── UnitConverterApp # 主应用组件
└── ConverterPage # 换算页面组件
总结
这个单位换算器的实现展示了几个关键的编程思想:
- 数据驱动UI:通过定义清晰的数据模型,UI可以自动适应不同类别的单位
- 统一的换算接口:除温度外,所有单位都使用相同的换算公式,代码复用性高
- 特殊情况特殊处理:温度换算的非线性关系需要单独处理
- 智能数值格式化:根据数值大小自动选择合适的显示格式
整个应用代码量不大,但功能完备,适合作为Flutter工具类应用的学习案例。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net