Flutter 实战:percentage_calculator 百分比计算器的三模式公式、实时计算与鸿蒙适配解析
Flutter 实战:percentage_calculator 百分比计算器的三模式公式、实时计算与 鸿蒙 适配解析
前言
百分比计算器看起来只是一个小工具,但它覆盖了 Flutter 应用里非常典型的一组能力:输入解析、模式切换、状态刷新、结果展示、快捷样例 和 跨端布局适配。percentage_calculator 正是这样一个适合拆解的小型项目,它用一个页面完成三类常用计算:
- 计算某个数值的百分之几。
- 计算一个数值占另一个数值的百分比。
- 计算新旧数值之间的百分比变化。
这篇文章会从真实 Flutter 实现出发,逐层拆解页面结构、状态字段、计算公式、UI 交互、测试思路与 OpenHarmony 适配观察。重点不是堆砌代码,而是把一个轻量工具应用怎样写得清晰、稳定、容易迁移讲完整。
小工具类应用的难点往往不在算法本身,而在“输入含义、公式分支、结果单位、异常边界”是否始终一致。

图示说明:上图用于展示 Flutter 页面布局在多端环境中的视觉组织方式。percentage_calculator 的核心界面由模式切换区、两个数字输入框、结果卡片和快捷示例组成。
一、项目定位与功能边界
1.1 应用定位
percentage_calculator 是一个基于 Flutter 和 Material 3 的百分比计算工具。它不是复杂业务系统,而是一个面向高频日常场景的单页应用。
典型使用场景包括:
- 购物折扣:计算 199 元的 20% 是多少。
- 成绩占比:计算 45 分占 60 分的百分比。
- 增长率:计算销售额从 50 增长到 60 后的变化率。
- 数据复核:通过快捷示例快速确认公式是否按预期工作。
1.2 功能清单
| 功能 | 页面表现 | 源码实现方式 |
|---|---|---|
| 三种计算模式 | 顶部三个模式标签 | _selectedMode 控制当前公式 |
| 双输入框 | Value、Percentage 或 New Value、Old Value | 两个 TextEditingController |
| 实时计算 | 输入变化后立即刷新结果 | onChanged 调用 _calculate() |
| 结果卡片 | 大字号展示计算结果 | Card + Text |
| 快捷示例 | 一键填充典型输入 | ActionChip 写入控制器 |
| Material 3 主题 | 蓝色种子色、现代控件风格 | ThemeData(useMaterial3: true) |
1.3 技术栈概览
| 技术点 | 项目用法 | 说明 |
|---|---|---|
| Flutter | 构建跨端 UI | 页面、布局、输入、卡片均来自 Flutter Widget |
| Dart | 编写状态与公式逻辑 | 使用类、方法、控制流和空安全语法 |
| Material 3 | 统一视觉风格 | 使用 ColorScheme.fromSeed 配置主题 |
| StatefulWidget | 承载交互状态 | 输入值、模式和结果都在 State 内维护 |
| flutter_test | 可做 Widget 测试 | 验证页面渲染、点击和结果变化 |
二、工程结构与运行方式
2.1 目录结构
项目结构保持了 Flutter 默认应用的清晰形态,核心代码集中在 lib/main.dart。
| 路径 | 作用 |
|---|---|
lib/main.dart |
应用入口、页面状态、计算公式和 UI 构建 |
pubspec.yaml |
Flutter SDK 依赖、Material 图标声明 |
test/widget_test.dart |
Widget 自动化测试入口 |
ohos/ |
OpenHarmony 端工程目录 |
analysis_options.yaml |
Dart 静态分析规则 |
2.2 环境准备
运行 Flutter 项目前,可以先确认本地环境:
flutter doctor
flutter pub get
flutter run
这三个命令分别用于检查环境、拉取依赖和启动应用。由于当前项目没有引入复杂三方插件,依赖恢复通常比较直接。
2.3 依赖结构
percentage_calculator 的依赖非常轻,核心只依赖 Flutter SDK 和常见的开发测试工具。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
这种依赖结构对跨端迁移很友好,因为业务逻辑、输入解析和界面渲染主要运行在 Dart 层,平台侧只需要关注窗口、输入法、字体和构建链路。
三、应用入口与根组件设计
3.1 main 函数
Flutter 应用从 main() 进入,然后通过 runApp 加载根组件。
import 'package:flutter/material.dart';
void main() {
runApp(const PercentageCalculatorApp());
}
这段代码非常短,但它明确了应用启动的根节点:PercentageCalculatorApp。
3.2 StatelessWidget 根组件
根组件不保存业务状态,只负责配置应用级信息。
class PercentageCalculatorApp extends StatelessWidget {
const PercentageCalculatorApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Percentage Calculator',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const PercentageCalculatorHomePage(title: 'Percentage Calculator'),
);
}
}
这里有三个关键点:
MaterialApp提供 Material 应用上下文。ThemeData统一颜色、组件和交互反馈。home指向真正承载业务的首页。
3.3 为什么根组件不使用 StatefulWidget
根组件只负责全局壳层,页面输入和结果变化都发生在首页内部。因此根组件使用 StatelessWidget 更合适,状态集中到 PercentageCalculatorHomePage 中,代码边界更清楚。
| 组件 | 是否持有状态 | 责任 |
|---|---|---|
PercentageCalculatorApp |
否 | 应用标题、主题、首页 |
PercentageCalculatorHomePage |
是 | 输入、选择、计算、渲染 |
四、页面状态设计
4.1 StatefulWidget 首页
首页使用 StatefulWidget,因为它必须响应用户输入并刷新页面。
class PercentageCalculatorHomePage extends StatefulWidget {
const PercentageCalculatorHomePage({super.key, required this.title});
final String title;
State<PercentageCalculatorHomePage> createState() =>
_PercentageCalculatorHomePageState();
}
title 作为不可变参数传入,真正会变化的数据放在 State 对象里。
4.2 State 中的核心字段
class _PercentageCalculatorHomePageState
extends State<PercentageCalculatorHomePage> {
final TextEditingController _valueController = TextEditingController();
final TextEditingController _percentController = TextEditingController();
double _result = 0;
int _selectedMode = 0;
}
四个字段完整描述了页面的当前状态。
| 字段 | 类型 | 含义 |
|---|---|---|
_valueController |
TextEditingController |
第一个输入框的文本控制器 |
_percentController |
TextEditingController |
第二个输入框的文本控制器 |
_result |
double |
当前公式计算出的结果 |
_selectedMode |
int |
当前选中的计算模式 |
4.3 模式编号设计
项目用 int 表示三种模式:
| 模式编号 | 页面文案 | 公式含义 |
|---|---|---|
0 |
Calculate % |
计算一个数的百分之几 |
1 |
X is % of Y |
计算 X 占 Y 的百分比 |
2 |
% Change |
计算新旧值变化率 |
这种写法简单直接,适合小型项目。后续如果模式继续增加,可以考虑把模式抽象成枚举,让语义更强。
五、三种百分比公式解析
5.1 计算某个数的百分之几
第一种模式对应“X% of Y”的问题。源码中第一个输入框表示数值,第二个输入框表示百分比。
_result = value * percent / 100;
例如输入:
| Value | Percentage | Result |
|---|---|---|
100 |
20 |
20.00 |
250 |
15 |
37.50 |
80 |
12.5 |
10.00 |
5.2 计算 X 占 Y 的百分比
第二种模式解决“X is what percent of Y”的问题。
_result = percent > 0 ? (value / percent) * 100 : 0;
这里的 percent 字段实际代表分母值。为了避免除数为 0,源码先判断 percent > 0,不满足时返回 0。
| Value | Base | Result |
|---|---|---|
15 |
50 |
30.00 |
45 |
60 |
75.00 |
2 |
8 |
25.00 |
5.3 计算百分比变化
第三种模式用于计算新旧值之间的变化率。
_result = percent > 0 ? ((value - percent) / percent) * 100 : 0;
在这个模式下,第一个输入框表示新值,第二个输入框表示旧值。
| New Value | Old Value | Result |
|---|---|---|
60 |
50 |
20.00% |
80 |
100 |
-20.00% |
120 |
80 |
50.00% |
公式分支越少,越要把输入标签写准确。否则用户看到的结果可能正确,但理解会出现偏差。
六、核心计算方法拆解
6.1 输入解析
源码通过 double.tryParse 将输入框文本转换为数字。
final value = double.tryParse(_valueController.text) ?? 0;
final percent = double.tryParse(_percentController.text) ?? 0;
这种写法有两个优点:
- 空字符串不会导致异常。
- 非法数字会自动回退为 0。
6.2 switch 分支
三种计算模式都集中在 _calculate() 方法里。
void _calculate() {
final value = double.tryParse(_valueController.text) ?? 0;
final percent = double.tryParse(_percentController.text) ?? 0;
setState(() {
switch (_selectedMode) {
case 0:
_result = value * percent / 100;
break;
case 1:
_result = percent > 0 ? (value / percent) * 100 : 0;
break;
case 2:
_result = percent > 0 ? ((value - percent) / percent) * 100 : 0;
break;
}
});
}
setState() 包住计算结果的写入,保证 _result 变化后界面立即重建。
6.3 计算方法的职责边界
_calculate() 只做三件事:
- 读取两个输入框。
- 根据模式执行公式。
- 写入新的结果状态。
它不直接创建 Widget,也不处理样式。这种分层让代码更容易阅读,也方便后续为公式单独补充测试。
七、模式切换控件设计
7.1 顶部模式容器
页面顶部使用一个浅蓝色容器包住三个模式标签。
Container(
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
_buildModeTab(0, 'Calculate %', Icons.calculate),
_buildModeTab(1, 'X is % of Y', Icons.percent),
_buildModeTab(2, '% Change', Icons.trending_up),
],
),
)
这种结构让三个模式在视觉上形成一个整体,用户能快速理解它们是互斥选项。
7.2 单个模式标签
单个模式标签由 _buildModeTab 生成。
Widget _buildModeTab(int mode, String label, IconData icon) {
final isSelected = _selectedMode == mode;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _selectedMode = mode),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(icon),
const SizedBox(height: 4),
Text(label, textAlign: TextAlign.center),
],
),
),
),
);
}
Expanded 保证三个标签均分宽度,GestureDetector 接收点击事件,isSelected 控制选中态样式。
7.3 选中态反馈
| 状态 | 背景 | 图标颜色 | 文本字重 |
|---|---|---|---|
| 未选中 | 透明 | 灰色 | 普通 |
| 已选中 | 蓝色 | 白色 | 加粗 |
清晰的选中态对计算器非常重要,因为公式切换会改变第二个输入框含义和最终结果含义。
八、输入框与动态标签
8.1 第一个输入框
第一个输入框在变化率模式下显示 New Value,其他模式显示 Value。
TextField(
controller: _valueController,
keyboardType: TextInputType.number,
style: const TextStyle(fontSize: 24),
decoration: InputDecoration(
labelText: _selectedMode == 2 ? 'New Value' : 'Value',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
onChanged: (_) => _calculate(),
)
动态标签能降低用户理解成本,尤其是 % Change 模式下,新值和旧值不能混淆。
8.2 第二个输入框
第二个输入框在变化率模式下显示 Old Value,其他模式显示 Percentage。
TextField(
controller: _percentController,
keyboardType: TextInputType.number,
style: const TextStyle(fontSize: 24),
decoration: InputDecoration(
labelText: _selectedMode == 2 ? 'Old Value' : 'Percentage',
suffixText: _selectedMode != 1 ? '%' : null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
onChanged: (_) => _calculate(),
)
suffixText 只在部分模式展示百分号。第二种模式里第二个输入框表示基准值,不应该显示 %。
8.3 输入行为表
| 模式 | 第一个输入框 | 第二个输入框 | 第二个输入框后缀 |
|---|---|---|---|
Calculate % |
Value |
Percentage |
% |
X is % of Y |
Value |
Percentage |
无 |
% Change |
New Value |
Old Value |
% |
这里可以看到,源码已经根据模式动态调整标签和后缀,但第二种模式的 labelText 仍显示 Percentage。从产品语义看,它代表基准值;文章分析时要如实说明,避免夸大成完整的商业级表单设计。
九、结果卡片实现
9.1 Card 结构
结果区域使用 Card 加渐变背景突出展示。
Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [Colors.blue.shade100, Colors.blue.shade50],
),
),
child: Column(
children: [
Text(_getResultLabel()),
const SizedBox(height: 8),
Text(_result.toStringAsFixed(2)),
],
),
),
)
卡片把结果从输入区域里独立出来,用户输入后能立刻看到反馈。
9.2 结果格式化
源码使用 toStringAsFixed(2) 固定保留两位小数。
Text(
_result.toStringAsFixed(2),
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
)
这种格式适合百分比和金额类场景,能避免结果位数忽长忽短造成视觉跳动。
9.3 单位显示
结果右侧单位由当前模式控制。
if (_selectedMode != 1) ...[
const SizedBox(width: 4),
Text(
_selectedMode == 2 ? '%' : '',
style: TextStyle(
fontSize: 24,
color: Colors.blue.shade700,
),
),
]
这里有一个细节:第一种模式不显示单位,第三种模式显示 %,第二种模式整段单位区域不展示。它符合当前源码表现,但如果把第二种模式视为百分比结果,后续也可以在产品层面让它显示百分号。
十、结果标题与用户理解
10.1 标题生成方法
结果卡片上方的说明来自 _getResultLabel()。
String _getResultLabel() {
switch (_selectedMode) {
case 0:
return '${_percentController.text}% of ${_valueController.text}';
case 1:
return '${_valueController.text} is what % of ${_percentController.text}';
case 2:
return 'Percentage change';
default:
return 'Result';
}
}
这段代码把输入值直接拼入说明文案,使结果与输入之间的关系更明确。
10.2 空输入时的显示
当输入为空时,标题中也会出现空字符串。例如第一种模式初始状态下可能显示 % of。这不是运行错误,只是初始文案不够完整。
| 状态 | 页面表现 | 影响 |
|---|---|---|
| 两个输入框为空 | 结果为 0.00 |
不会崩溃 |
| 只输入第一个值 | 使用另一个默认值 0 计算 | 结果可预测 |
| 输入非数字 | 解析为 0 | 避免异常 |
10.3 说明文案的价值
对工具类页面来说,结果数字必须和公式解释同时出现。否则用户很难判断当前看到的是“比例结果”还是“变化率结果”。
十一、快捷示例 ActionChip
11.1 示例区域
页面底部使用 Wrap 展示三个快捷样例。
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildExampleChip('20% of 100 = 20', 0, '100', '20'),
_buildExampleChip('15 is 30% of 50', 1, '15', '50'),
_buildExampleChip('Change: 50 to 60 = 20%', 2, '60', '50'),
],
)
Wrap 比 Row 更适合小屏,因为内容超出宽度后可以自动换行。
11.2 示例按钮实现
Widget _buildExampleChip(
String label,
int mode,
String value,
String percent,
) {
return ActionChip(
label: Text(label, style: const TextStyle(fontSize: 12)),
onPressed: () {
setState(() {
_selectedMode = mode;
_valueController.text = value;
_percentController.text = percent;
});
_calculate();
},
);
}
点击示例后,源码会同时更新模式和两个输入框,再调用 _calculate() 重新计算结果。
11.3 快捷示例的工程价值
快捷示例不只是体验优化,也有工程价值:
- 可以作为人工验收用例。
- 可以帮助读者理解三种公式。
- 可以降低首次打开应用时的空白感。
- 可以为 Widget 测试提供明确输入样本。
十二、布局与响应式处理
12.1 SingleChildScrollView
页面主体使用 SingleChildScrollView 包裹。
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 模式、输入框、结果卡片、快捷示例
],
),
)
这样在小屏或横屏场景下,内容不会因为高度不足直接溢出。
12.2 Column 与 stretch
crossAxisAlignment: CrossAxisAlignment.stretch 让子组件横向铺满父容器。
| 布局元素 | 作用 |
|---|---|
Column |
从上到下排列页面模块 |
SizedBox |
控制模块间距 |
Expanded |
让三个模式标签均分宽度 |
Wrap |
让快捷示例自动换行 |
12.3 OpenHarmony 上的布局关注点
OpenHarmony 设备形态可能覆盖手机、平板、窗口化屏幕等场景。这个页面的布局优势是纵向结构清晰,但仍要关注:
- 三个模式标签在窄屏下的文字是否换行自然。
- 大号结果数字在极端值下是否挤压卡片。
- 输入框弹出键盘后页面是否仍可滚动。
ActionChip在不同字体缩放下是否保持可点击。
十三、生命周期与资源释放
13.1 dispose 方法
两个 TextEditingController 都需要在页面销毁时释放。
void dispose() {
_valueController.dispose();
_percentController.dispose();
super.dispose();
}
这段代码体现了 Flutter 中控制器类资源管理的基本规范。
13.2 为什么要释放控制器
TextEditingController 会持有文本状态和监听能力。如果页面销毁后控制器还被长期保留,可能造成无意义的内存占用。
| 对象 | 是否需要释放 | 原因 |
|---|---|---|
TextEditingController |
需要 | 持有输入状态和监听能力 |
普通 double 字段 |
不需要 | 值类型字段随 State 回收 |
普通 int 字段 |
不需要 | 不持有外部资源 |
13.3 生命周期顺序
一个典型页面生命周期可以理解为:
创建 Widget
创建 State
执行 build
响应用户输入
多次 setState
页面销毁
执行 dispose
这种顺序对理解 Flutter 交互页面很重要。计算器页面虽然简单,但已经包含完整生命周期。
十四、异常输入与边界场景
14.1 空输入
空输入会被 double.tryParse 解析失败,然后通过 ?? 0 回退为 0。
final emptyValue = double.tryParse('') ?? 0;
这保证用户清空输入框时应用不会抛异常。
14.2 除数为 0
第二、第三种模式都会涉及除法。源码通过条件表达式规避除数为 0。
final result = base > 0 ? (value / base) * 100 : 0;
这个策略简单稳定:当分母不是正数时直接返回 0。
14.3 负数与小数
keyboardType: TextInputType.number 表达了数字输入意图,但不同平台输入法对负号和小数点支持不完全一致。业务公式本身可以处理小数,变化率也可以出现负值。
| 输入类型 | 源码处理 | 结果 |
|---|---|---|
| 空字符串 | 回退为 0 | 稳定 |
| 普通整数 | 正常计算 | 稳定 |
| 小数 | 正常计算 | 稳定 |
| 非数字字符 | 回退为 0 | 稳定 |
| 分母为 0 | 返回 0 | 避免异常 |
十五、Widget 测试设计
15.1 页面渲染测试
可以先验证应用能正常启动并显示标题。
import 'package:flutter_test/flutter_test.dart';
import '../lib/main.dart';
void main() {
testWidgets('percentage calculator renders title', (tester) async {
await tester.pumpWidget(const PercentageCalculatorApp());
expect(find.text('Percentage Calculator'), findsWidgets);
expect(find.text('Calculate %'), findsOneWidget);
expect(find.text('% Change'), findsOneWidget);
});
}
测试重点是根组件、标题和模式按钮是否可见。
15.2 快捷示例测试
快捷示例非常适合做端到端式 Widget 测试。
testWidgets('example chip fills inputs and calculates result', (tester) async {
await tester.pumpWidget(const PercentageCalculatorApp());
await tester.tap(find.text('20% of 100 = 20'));
await tester.pump();
expect(find.text('20.00'), findsOneWidget);
});
这个测试覆盖了点击、状态写入、公式计算和结果渲染。
15.3 模式切换测试
testWidgets('change mode shows percentage change label', (tester) async {
await tester.pumpWidget(const PercentageCalculatorApp());
await tester.tap(find.text('% Change'));
await tester.pump();
expect(find.text('New Value'), findsOneWidget);
expect(find.text('Old Value'), findsOneWidget);
});
这类测试能验证 UI 文案是否随着 _selectedMode 同步更新。
15.4 测试命令
flutter test
如果测试代码与当前组件类名不一致,测试会在编译阶段暴露问题。对于 Flutter 小应用来说,保持测试入口和真实根组件一致非常关键。
十六、OpenHarmony 适配观察
16.1 适配优势
percentage_calculator 的主体逻辑全部位于 Dart 层,没有摄像头、定位、蓝牙、数据库等原生插件依赖,因此跨端适配重点相对集中。
| 维度 | 当前项目情况 | 适配影响 |
|---|---|---|
| 原生插件 | 无复杂插件 | 平台侧风险较低 |
| 输入控件 | 标准 TextField |
关注键盘与数字格式 |
| 图标 | Material Icons | 关注字体资源和渲染 |
| 布局 | 标准 Flutter Widget | 关注屏幕宽度和字体缩放 |
| 计算逻辑 | Dart 公式 | 多端结果一致性较高 |
16.2 OpenHarmony 构建关注点
OpenHarmony 适配时,除了 Flutter 代码本身,还要关注平台工程和构建链路。
flutter clean
flutter pub get
flutter build hap
具体命令取决于使用的 OpenHarmony Flutter 适配环境。对这个项目来说,更重要的是验证运行效果:
- 首页是否能正常启动。
- 输入框是否能调起数字键盘。
- 三种模式点击是否流畅。
- 结果卡片是否在不同屏幕宽度下完整显示。
- 快捷示例是否能正确写入输入框。
16.3 多端一致性
跨端一致性不是只看能否编译,还要看交互语义是否一致。
| 场景 | Android | OpenHarmony | 关注点 |
|---|---|---|---|
| 输入数字 | 支持 | 需验证 | 键盘布局和小数点 |
| 点击模式 | 支持 | 需验证 | 触控反馈 |
| 滚动页面 | 支持 | 需验证 | 键盘遮挡后的滚动 |
| 展示图标 | 支持 | 需验证 | Material 图标字体 |
| 结果计算 | 支持 | 支持 | Dart 层逻辑一致 |
十七、可维护性与后续演进
17.1 当前结构的优点
percentage_calculator 当前代码集中,适合教学和快速理解。
- 入口清晰:
main()到MaterialApp再到首页。 - 状态集中:输入、模式和结果都在同一个 State 中。
- 公式集中:三种模式统一由
_calculate()处理。 - UI 简洁:页面模块按从上到下的顺序排列。
17.2 可以演进的方向
随着功能增加,可以把计算逻辑从 Widget 中抽离。
double calculatePercentOf(double value, double percent) {
return value * percent / 100;
}
double calculateRatio(double value, double base) {
return base > 0 ? (value / base) * 100 : 0;
}
double calculateChange(double current, double previous) {
return previous > 0 ? ((current - previous) / previous) * 100 : 0;
}
抽离后,公式可以直接做单元测试,Widget 测试只关注页面交互。
17.3 模式枚举
如果希望提高可读性,可以用枚举替代裸 int。
enum PercentageMode {
percentOf,
ratio,
change,
}
枚举能避免 0、1、2 这种魔法数字在项目变大后造成理解成本。
十八、常见问题与优化建议
18.1 为什么第二种模式结果没有显示百分号
源码中 _selectedMode != 1 时才展示右侧单位区域,因此第二种模式不显示 %。这属于当前实现的真实表现。由于第二种模式计算结果本质上也是百分比,产品层可以根据需要统一补充单位展示。
18.2 为什么输入非法字符不会崩溃
因为源码使用了 double.tryParse,解析失败后通过 ?? 0 回退。这个处理对实时输入尤其重要,用户在输入过程中经常会出现临时空值。
18.3 为什么使用 SingleChildScrollView
计算器页面看起来不长,但键盘弹出、字体放大、横屏或小屏设备都可能让可用高度变小。滚动容器能让页面在更多设备上保持可操作。
18.4 为什么快捷示例点击后还要调用计算方法
示例按钮会直接修改控制器文本。为了让结果立即同步,需要在写入文本后调用 _calculate()。这样用户点击后可以直接看到对应结果。
18.5 为什么没有引入状态管理框架
当前页面只有四个核心状态字段,使用 StatefulWidget 已经足够。过早引入状态管理框架会增加样板代码,不一定提升可维护性。
18.6 小数精度如何处理
页面展示层使用 toStringAsFixed(2),它解决的是显示格式问题。对于财务级精度场景,还需要额外考虑十进制精度库、舍入规则和本地化格式。
总结
percentage_calculator 用很少的代码完成了一个 Flutter 小工具应用的完整闭环:从 MaterialApp 启动,到 StatefulWidget 管理输入和模式,再到 _calculate() 执行三种百分比公式,最后通过结果卡片和快捷示例完成即时反馈。
这个项目的价值在于结构清晰、边界明确,非常适合用来理解 Flutter 交互页面的基本写法。对于 OpenHarmony 跨端适配来说,它的业务逻辑主要位于 Dart 层,没有复杂原生插件依赖,因此验证重点集中在输入法、字体、布局、主题和构建链路上。
如果要继续提升它,可以从三个方向入手:抽离公式函数、补充 Widget 测试、优化第二种模式的输入标签和单位显示。这样既能保持小工具的轻量感,也能让代码更适合长期维护。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!
相关资源:
更多推荐

所有评论(0)