在这里插入图片描述

前面我们已经把:

  • 记录的录入与编辑
  • 统计页的算法与图表
  • 数据的导入导出

这些“硬功能”跑通了。

从这一篇开始,我们把注意力放到一个看似不起眼、但会影响整套体验的点:

  • 单位与币种

你不一定马上支持所有国家地区,
但至少要做到:

  • 配置项有地方改
  • 改完立即生效
  • 下次启动还能记住

这篇文章完全基于 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 统一负责。

这里有一个常见问题:

  • vString?

项目的 setUnits 参数也是 String?
因此即使 v 为 null,这里也能安全传递。
控制器内部会用 if (fuel != null) 防御。


13. 选项值是“协议”,展示文本是“本地化”

你会看到 dropdown 的 value 是:

  • km/mi
  • L/gal
  • CNY/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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐