flutter_for_openharmonyFillUp -油耗追踪器app实战+单位币种实现
展示里程、油量、金额的单位文本。导出用户导出 CSV/JSON 时,希望字段含义一致。未来扩展如果你做“统计/报表”,单位统一会影响可理解性。因此项目里把它放到了里,作为设置系统的一部分。

前面我们已经把:
- 记录的录入与编辑
- 统计页的算法与图表
- 数据的导入导出
这些“硬功能”跑通了。
从这一篇开始,我们把注意力放到一个看似不起眼、但会影响整套体验的点:
- 单位与币种
你不一定马上支持所有国家地区,
但至少要做到:
- 配置项有地方改
- 改完立即生效
- 下次启动还能记住
这篇文章完全基于 lib/app/fillup_app.dart 的真实代码。
每段代码后紧跟解释。
1. 为什么单位/币种不是“最后再做”的装饰
单位与币种通常会影响三类地方:
- 展示
里程、油量、金额的单位文本。 - 导出
用户导出 CSV/JSON 时,希望字段含义一致。 - 未来扩展
如果你做“统计/报表”,单位统一会影响可理解性。
因此项目里把它放到了 FillUpSettingsController 里,
作为设置系统的一部分。
2. 设置控制器:用 RxString 保存 distance/fuel/currency
项目把单位与币种作为设置项,
与主题放在同一个控制器:FillUpSettingsController。
真实代码如下(摘录):
class FillUpSettingsController extends GetxController {
final Rx<ThemeMode> _themeMode = ThemeMode.system.obs;
final RxString _distanceUnit = 'km'.obs;
final RxString _fuelUnit = 'L'.obs;
final RxString _currency = 'CNY'.obs;
ThemeMode get themeMode => _themeMode.value;
String get distanceUnit => _distanceUnit.value;
String get fuelUnit => _fuelUnit.value;
String get currency => _currency.value;
解释:
- 默认距离单位是
km - 默认燃油单位是
L - 默认币种是
CNY
这里把值定义为字符串,而不是 enum,
属于偏务实的选择:
- SharedPreferences 存字符串更直接
- 未来新增选项不需要做复杂迁移
同时你会看到对外暴露的是 getter,
而不是把 _distanceUnit 这种 Rx 直接暴露出去。
好处是:
- UI 用起来更干净
- 控制器有更强的封装边界
3. load:从 SharedPreferences 恢复单位与币种
设置必须持久化。
项目在 load() 里一次性恢复主题、单位、币种。
这里我们重点看单位与币种相关部分。
真实代码如下(完整摘录,含主题解析,但你关注的是后三行):
Future<void> load() async {
final prefs = await SharedPrefs.instance.get();
final theme = prefs.getString('theme_mode') ?? 'system';
ThemeMode parsed;
switch (theme) {
case 'light':
parsed = ThemeMode.light;
break;
case 'dark':
parsed = ThemeMode.dark;
break;
default:
parsed = ThemeMode.system;
break;
}
_themeMode.value = parsed;
Get.changeThemeMode(parsed);
_distanceUnit.value = prefs.getString('distance_unit') ?? 'km';
_fuelUnit.value = prefs.getString('fuel_unit') ?? 'L';
_currency.value = prefs.getString('currency') ?? 'CNY';
}
解释:
distance_unit/fuel_unit/currency三个 key 都带默认值。- 这意味着即使用户从未进入设置页,App 也有可用的展示值。
你在这里能看到一种“极简但稳定”的策略:
- 读取不到就用默认
- 读到了就按存储值覆盖
这种策略的最大优点是:
- 不会因为 prefs 缺失导致 UI 出现 null
4. save:把单位与币种写回 SharedPreferences
与 load() 对称的是 save()。
它负责把当前设置写入 prefs。
真实代码如下(完整摘录,主题写入也在里面):
Future<void> save() async {
final prefs = await SharedPrefs.instance.get();
String theme;
switch (_themeMode.value) {
case ThemeMode.light:
theme = 'light';
break;
case ThemeMode.dark:
theme = 'dark';
break;
case ThemeMode.system:
theme = 'system';
break;
}
await prefs.setString('theme_mode', theme);
await prefs.setString('distance_unit', _distanceUnit.value);
await prefs.setString('fuel_unit', _fuelUnit.value);
await prefs.setString('currency', _currency.value);
}
解释:
- 主题写入是把 ThemeMode 映射成字符串。
- 单位与币种直接写字符串即可。
这里有一个值得保留的点:
save()不拆分成多个函数。
它让设置系统变成一个“整体状态”。
这对未来扩展很友好:
- 你新增一个设置项,只要加一行 setString。
5. setUnits:一个入口更新 3 个字段并触发 save
UI 层最怕的是:
- 三个下拉各改各的
- 改完还要记得调用 save
项目用 setUnits 把这件事收敛起来。
真实代码如下:
Future<void> setUnits({String? distance, String? fuel, String? currency}) async {
if (distance != null) _distanceUnit.value = distance;
if (fuel != null) _fuelUnit.value = fuel;
if (currency != null) _currency.value = currency;
await save();
}
解释:
- 三个参数都是可选的。
- UI 改哪个就传哪个。
- 最后统一
await save()。
这一段看起来简单,但它直接提升了可维护性:
- 你不用在 UI 里到处写
save() - 也减少了“忘记保存导致下次启动丢设置”的 bug
6. onInit:设置控制器启动即 load
为了保证全局任何页面都能读到正确单位,
项目在控制器初始化时就加载 prefs。
真实代码如下:
void onInit() {
super.onInit();
load();
}
解释:
- 这让
distanceUnit/fuelUnit/currency在 UI 使用前就准备好。 - 即使用户不进入设置页,展示也不会出问题。
7. 功能中心注册:units_currency 对应“单位/币种”页面
项目把单位/币种作为 Feature 页的一项。
它在注册表里对应 units_currency。
真实代码如下(摘录):
class FillUpFeatureRegistry {
static const List<FillUpFeature> features = <FillUpFeature>[
FillUpFeature(id: 'units_currency', title: '单位/币种', icon: Icons.straighten),
FillUpFeature(id: 'theme_appearance', title: '主题外观', icon: Icons.palette),
解释:
- 这说明
units_currency不需要单独写路由。 - 只要在注册表里出现,它就自动成为
/feature/units_currency。
8. FeaturePage 分发:units_currency 走 _buildUnits
Feature 页根据 id 分发不同内容。
单位/币种对应 _buildUnits(context)。
真实代码如下(摘录):
if (feature.id == 'units_currency') ..._buildUnits(context),
if (feature.id == 'theme_appearance') ..._buildTheme(context),
解释:
_buildUnits返回List<Widget>。- FeaturePage 使用
...把它们展开到页面中。
9. UI:_buildUnits 的三个 Dropdown
_buildUnits 是单位/币种功能的 UI 核心。
它做了三件事:
- 距离单位:km/mi
- 燃油单位:L/gal
- 币种:CNY/USD/HKD
真实代码如下(完整摘录):
List<Widget> _buildUnits(BuildContext context) {
final settings = Get.find<FillUpSettingsController>();
return <Widget>[
const _SectionHeader(title: '单位与币种'),
SizedBox(height: 10.h),
Obx(() {
return Column(
children: <Widget>[
DropdownButtonFormField<String>(
value: settings.distanceUnit,
items: const <DropdownMenuItem<String>>[
DropdownMenuItem(value: 'km', child: Text('公里 km')),
DropdownMenuItem(value: 'mi', child: Text('英里 mi')),
],
onChanged: (v) => settings.setUnits(distance: v),
decoration: const InputDecoration(labelText: '距离单位'),
),
SizedBox(height: 12.h),
DropdownButtonFormField<String>(
value: settings.fuelUnit,
items: const <DropdownMenuItem<String>>[
DropdownMenuItem(value: 'L', child: Text('升 L')),
DropdownMenuItem(value: 'gal', child: Text('加仑 gal')),
],
onChanged: (v) => settings.setUnits(fuel: v),
decoration: const InputDecoration(labelText: '燃油单位'),
),
SizedBox(height: 12.h),
DropdownButtonFormField<String>(
value: settings.currency,
items: const <DropdownMenuItem<String>>[
DropdownMenuItem(value: 'CNY', child: Text('人民币 CNY')),
DropdownMenuItem(value: 'USD', child: Text('美元 USD')),
DropdownMenuItem(value: 'HKD', child: Text('港币 HKD')),
],
onChanged: (v) => settings.setUnits(currency: v),
decoration: const InputDecoration(labelText: '币种'),
),
],
);
}),
];
}
下面按 UI 体验与工程取舍解释。
10. 为什么 UI 用 Obx 包住 Column
Obx(() {
return Column(
children: <Widget>[
...
],
);
}),
解释:
distanceUnit/fuelUnit/currency都是响应式状态。- 当用户修改任意一个下拉,
Obx会触发重建。
这让 UI 能做到:
- 改完立刻展示当前值
- 不需要手动 setState
11. Dropdown 的 value 直接用 settings.xxx
例如距离单位:
value: settings.distanceUnit,
解释:
- settings 对外暴露的是 getter。
- UI 不需要关心 Rx。
这也是为什么我们在第 2 节强调“封装 Rx”。
12. onChanged 直接调用 setUnits:单点收敛
例如燃油单位:
onChanged: (v) => settings.setUnits(fuel: v),
解释:
- UI 只传变更项。
- 持久化由 setUnits 统一负责。
这里有一个常见问题:
v是String?。
项目的 setUnits 参数也是 String?。
因此即使 v 为 null,这里也能安全传递。
控制器内部会用 if (fuel != null) 防御。
13. 选项值是“协议”,展示文本是“本地化”
你会看到 dropdown 的 value 是:
km/miL/galCNY/USD/HKD
而 child 是中文说明 + value:
公里 km
这是一种比较稳妥的方式:
- value 是机器可处理的协议
- 文本是面向用户的展示
当你未来做本地化时:
- 只改 child 文本即可
- value 不必变化
14. 单位改了以后,哪些地方应该跟着改
当前项目里,单位/币种更多是“设置项落地”。
它们真正被消费的地方,你可以按优先级这样推进:
- 显示层
比如把km/mi拼到里程文案里。
在本项目里,我们先把“设置链路”做对:能改、能存、能恢复。
15. 小结
这一篇我们把“单位/币种”的实现闭环讲清楚了:
- 设置项在
FillUpSettingsController里用RxString保存 load()从 SharedPreferences 恢复默认值save()把当前值写回setUnits()收敛 UI 更新与持久化- Feature 页用
_buildUnits提供三个 dropdown 修改入口
文章底部添加社区引导:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)