flutter_for_openharmonyFillUp -油耗追踪器app实战+主题外观实现
这一篇我们只讲主题外观的实现链路,所有代码都来自。每段代码后紧跟解释。

当你把“记录/统计/导入导出”做得差不多之后,用户会开始关注体验层:
- 白天用浅色,晚上用深色
- 或者让 App 跟随系统
主题外观看起来像 UI 小功能,但它牵扯三件工程事:
- ThemeMode 如何全局生效
- 用户选择如何持久化
- 设置页面如何做到“改完立刻见效”
这一篇我们只讲主题外观的实现链路,所有代码都来自 lib/app/fillup_app.dart。
每段代码后紧跟解释。
1. 全局主题配置:在 FillUpApp 同时提供 theme 与 darkTheme
主题外观一定不是某个页面的局部样式。
项目在根部 GetMaterialApp 里同时配置了浅色与深色主题。
真实代码如下(节选):
return GetMaterialApp(
title: 'FillUp 油耗追踪器',
debugShowCheckedModeBanner: false,
initialBinding: FillUpBindings(),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system,
解释:
theme:浅色主题的默认配置darkTheme:深色主题配置(显式brightness: Brightness.dark)themeMode: ThemeMode.system:默认跟随系统
这里有一个很重要的约束:
- 你只配置
theme而不配置darkTheme,深色模式下体验会非常不可控。
项目选择 ColorScheme.fromSeed(seedColor: Colors.teal) 也有好处:
- 你只要一个主色,就能生成相对完整的颜色体系
- 不需要手工维护一大堆颜色常量
2. 绑定注入:FillUpSettingsController 必须是全局单例
主题控制器如果不是全局的,设置页改了也无法影响其它页面。
项目在 FillUpBindings 里注入 FillUpSettingsController。
真实代码如下:
class FillUpBindings extends Bindings {
void dependencies() {
Get.put(FillUpShellController());
Get.put(FillUpSettingsController());
Get.put(VehiclesController());
Get.put(FillUpsController());
}
}
解释:
Get.put(FillUpSettingsController())让它在整个 App 生命周期内可用- 任何页面都可以
Get.find<FillUpSettingsController>()
这种结构也意味着:
- themeMode 的变更必须是“即时生效”,否则用户会觉得设置无效
3. SettingsController 的数据模型:用 Rx 承载主题状态
项目用 GetX 的 Rx 来承载主题模式:
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;
解释:
_themeMode的默认值是ThemeMode.system- 对外暴露
themeModegetter,而不是把 Rx 直接暴露给 UI
把 Rx 封装起来的价值是:
- UI 不需要知道
_themeMode是 Rx - 未来你要换状态管理或加校验,改动面更小
4. setThemeMode:状态更新 + 立即切换主题
主题外观最容易做错的一点是:
- 你只更新了本地变量,却没有让 App 立即切换主题。
项目的 setThemeMode 同时做两件事:
void setThemeMode(ThemeMode mode) {
_themeMode.value = mode;
Get.changeThemeMode(mode);
}
解释:
_themeMode.value = mode:更新本地状态Get.changeThemeMode(mode):让 GetMaterialApp 立即切换到指定 ThemeMode
这里的关键是第二句。
如果你缺了它,UI 只能等到重启应用才会生效。
另外注意:
setThemeMode并不负责持久化- 持久化是
save()的职责
职责分离能让代码更清晰。
5. load:从 SharedPreferences 恢复主题设置
主题模式必须持久化。
项目使用 SharedPreferences,并把存储值设计成字符串:
lightdarksystem
真实代码如下:
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';
}
这段代码里跟主题相关的核心点有三个。
5.1 先取字符串,再解析
final theme = prefs.getString('theme_mode') ?? 'system';
解释:
- SharedPreferences 存字符串最稳定
- 即使以后扩展,也不需要迁移复杂对象
5.2 switch 做解析映射
switch (theme) {
case 'light':
parsed = ThemeMode.light;
break;
case 'dark':
parsed = ThemeMode.dark;
break;
default:
parsed = ThemeMode.system;
break;
}
解释:
- default 回落到
ThemeMode.system - 避免 prefs 存了非法值导致崩溃
5.3 load 里也要调用 Get.changeThemeMode
_themeMode.value = parsed;
Get.changeThemeMode(parsed);
解释:
- load 不仅恢复状态,也要让主题立即生效
- 否则用户明明之前选了深色,打开应用却仍然是浅色
6. save:把 ThemeMode 写回 SharedPreferences
保存逻辑与 load 是对称的。
项目仍然把 ThemeMode 转成字符串存储。
真实代码如下:
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);
}
解释:
save()只负责把当前状态写入 prefs- 写入顺序无强制要求,但保持可读性很重要
save() 是 async,UI 侧一般不需要 await,但要避免在高频场景里反复触发。
7. onInit:控制器初始化时自动 load
控制器是全局单例,所以它初始化时就应该恢复设置。
真实代码如下:
void onInit() {
super.onInit();
load();
}
解释:
load()在初始化时执行- 这保证用户进入任何页面时主题状态已经准备好
主题切换尽量在应用启动早期完成,避免出现 UI 一闪(先浅后深)。
8. 设置页的主题选择:RadioListTile 三选一
项目在 SettingsPage 里放了一个卡片,用单选的方式让用户选主题模式。
真实代码如下(完整摘录):
Card(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
child: Obx(() {
final mode = settings.themeMode;
return Column(
children: <Widget>[
RadioListTile<ThemeMode>(
value: ThemeMode.system,
groupValue: mode,
onChanged: (v) {
if (v != null) settings.setThemeMode(v);
settings.save();
},
title: const Text('跟随系统'),
),
RadioListTile<ThemeMode>(
value: ThemeMode.light,
groupValue: mode,
onChanged: (v) {
if (v != null) settings.setThemeMode(v);
settings.save();
},
title: const Text('浅色'),
),
RadioListTile<ThemeMode>(
value: ThemeMode.dark,
groupValue: mode,
onChanged: (v) {
if (v != null) settings.setThemeMode(v);
settings.save();
},
title: const Text('深色'),
),
],
);
}),
),
),
解释这段 UI 的几个关键点。
8.1 用 Obx 监听 themeMode
final mode = settings.themeMode;
解释:
settings.themeMode变更后,Obx 内部会自动重建- 这保证选中的 Radio 立即更新
8.2 onChanged 做两件事:setThemeMode + save
if (v != null) settings.setThemeMode(v);
settings.save();
解释:
setThemeMode负责立即生效save负责持久化
注意这里并没有 await。
原因是:
- save 是 I/O
- UI 不应该被阻塞
8.3 为什么这里用 Radio 而不是 Switch
Switch 只有两态。
但 ThemeMode 有三态:
- system
- light
- dark
因此 RadioListTile 更符合语义。
9. 功能中心里的“主题外观”页:_buildTheme 的简化开关
项目里同时提供了一个 feature 页版本的主题开关。
它不是三选一,而是一个“深色模式”的简单开关。
真实代码如下:
List<Widget> _buildTheme(BuildContext context) {
final settings = Get.find<FillUpSettingsController>();
return <Widget>[
const _SectionHeader(title: '主题外观'),
SizedBox(height: 10.h),
Obx(() {
final mode = settings.themeMode;
return Column(
children: <Widget>[
SwitchListTile(
value: mode == ThemeMode.dark,
onChanged: (v) {
settings.setThemeMode(v ? ThemeMode.dark : ThemeMode.light);
settings.save();
},
title: const Text('深色模式'),
subtitle: const Text('仅影响 UI 展示'),
),
],
);
}),
];
}
解释:
SwitchListTile的 value 用mode == ThemeMode.dark判断- onChanged 时在 dark/light 之间切换
你会发现它没有提供 system 选项。
这是一个典型的“功能中心占位页”取舍:给一个最常用的开关,不在 feature 页里塞满配置项;subtitle 也明确“仅影响 UI 展示”。
10. 小结
这一篇我们把主题外观实现串成完整链路:
FillUpApp同时配置theme/darkTheme/themeModeFillUpSettingsController用Rx<ThemeMode>承载状态setThemeMode负责立即生效(Get.changeThemeMode)load/save负责 SharedPreferences 持久化- 设置页用 RadioListTile 做三态选择,feature 页用 SwitchListTile 做简化开关
文章底部添加社区引导:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)