Flutter三方库适配OpenHarmony【unit_converter】单位转换器项目完整实战

Flutter三方库适配OpenHarmony【unit_converter】单位转换器项目完整实战

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

unit_converter 是一个基于 Flutter 的单位转换器项目,核心代码位于 lib/main.dart。项目通过结构化模型描述转换分类和单位列表,支持 Length、Weight、Temperature、Volume 四类单位转换,并使用输入框、横向 FilterChipDropdownButtonFormField、交换按钮和结果卡片组成完整工具界面。

这个项目适合讲解 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 技术目标

本文重点拆解以下内容:

  1. Flutter 应用入口和绿色 Material 3 主题。
  2. ConversionCategoryConversionUnit 的模型设计。
  3. 四类单位数据如何组织。
  4. TextEditingController.addListener 如何实现实时转换。
  5. 普通单位如何通过基准单位完成换算。
  6. 温度单位为什么不能直接使用比例系数换算。
  7. FilterChip 如何实现横向分类选择。
  8. DropdownButtonFormField 如何实现单位选择。
  9. 交换按钮如何互换来源和目标单位。
  10. 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 上运行时,首屏应看到:

  1. 顶部 AppBar 标题为 Unit Converter
  2. 横向分类 Chip 默认选中 Length。
  3. 输入框默认值为 1
  4. From 默认是 Meter。
  5. To 默认是 Kilometer。
  6. 结果卡片显示 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 使用 nameicon
  • 下拉菜单使用 namesymbol
  • 普通转换公式使用 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

  1. 用户修改输入值。
  2. _inputController 监听器触发。
  3. _convert 解析输入。
  4. 根据分类选择普通转换或温度转换。
  5. setState 更新 _result
  6. 结果卡片刷新。

七、普通单位转换逻辑

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 下拉菜单

下拉菜单适配需要确认:

  1. 点击 From 能展开单位列表。
  2. 点击 To 能展开单位列表。
  3. 选中某个单位后菜单关闭。
  4. 结果立即刷新。
  5. 长单位名称不会严重溢出。

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 mi1609.344 m
重量转换 选择 Kilogram 到 Gram 1 kg1000 g
温度转换 选择 Celsius 到 Fahrenheit 0 C32 F
体积转换 选择 Liter 到 Milliliter 1 L1000 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 工具逐步演进为更完整的跨平台应用。

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


相关资源:

相关推荐
FrameNotWork6 小时前
HarmonyOS 6.1 云应用客户端适配实战(二):Native Window 视频渲染
华为·音视频·harmonyos
G_dou_7 小时前
Flutter三方库适配OpenHarmony【coin_flip】抛硬币动画项目完整实战
flutter·harmonyos
再见6587 小时前
HarmonyOS NEXT 实战:从零开发一款「随笔记」应用
华为·harmonyos
jingling5557 小时前
Flutter | 商城项目完整实战
前端·flutter·前端框架
再见6588 小时前
HarmonyOS NEXT 实战:从零开发一个专业秒表应用
华为·harmonyos
想你依然心痛10 小时前
HarmonyOS 6(API 23)实战:打造“光码智学舱“——AI编程学习新范式
学习·ar·ai编程·harmonyos·智能体
慧海灵舟12 小时前
鸿蒙南向开发教程 Day 4:OpenHarmony 软件定时器
华为·harmonyos
FrameNotWork12 小时前
HarmonyOS 6.1 云应用客户端适配实战(五):日志调试与问题排查
华为·音视频·harmonyos
大雷神12 小时前
第40篇|美颜预设:自然、人像、清透如何变成可解释选项
harmonyos