Flutter for OpenHarmony:从零搭建今日资讯App(十五)主题切换功能的实现
Flutter应用主题切换功能实现摘要: 本文介绍了Flutter应用中实现主题切换功能的完整方案。系统支持三种主题模式:浅色、深色和跟随系统,通过ThemeProvider状态管理类统一管理。关键实现包括:使用SharedPreferences持久化用户选择;通过ColorScheme.fromSeed基于种子颜色生成协调的配色方案;Material 3设计规范的适配;以及全局主题状态的监听与更

深色模式已经成为现代应用的标配。用户在晚上看手机,刺眼的白色背景很不舒服,深色模式就能解决这个问题。今天咱们就来聊聊,怎么给应用加上主题切换功能。
主题切换的三种模式
Flutter的主题系统支持三种模式,每种都有自己的使用场景。
浅色模式就是传统的白色背景、黑色文字。适合白天使用,光线充足的环境下看得清楚。大部分应用默认都是浅色模式。
深色模式是黑色背景、白色文字。适合晚上使用,减少屏幕亮度,保护眼睛。很多用户晚上刷手机都喜欢开深色模式。
跟随系统是最智能的选择。白天系统是浅色,应用就用浅色;晚上系统切换到深色,应用也自动切换。用户不用手动调整,很方便。
咱们的应用三种模式都支持,让用户自己选择。
ThemeProvider的设计
主题状态需要全局共享,所以用Provider管理。先看看基本结构:
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
_themeMode是私有变量,存储当前的主题模式。默认值是ThemeMode.system,跟随系统,这是最合理的默认值。
themeMode是公开的getter,外部只能读取,不能直接修改。所有修改都要通过setThemeMode方法,这样才能保证数据持久化和UI更新。
构造函数里加载保存的主题:
ThemeProvider() {
_loadThemeMode();
}
应用启动时Provider就会创建,立即从本地加载用户上次选择的主题。这样用户下次打开应用,主题还是上次的设置。
从本地加载主题设置
加载主题的逻辑稍微复杂点,因为要处理枚举和字符串的转换:
Future<void> _loadThemeMode() async {
final prefs = await SharedPreferences.getInstance();
final themeModeString = prefs.getString('themeMode') ?? 'system';
首先从SharedPreferences读取保存的主题字符串。如果没有保存过,默认用’system’。注意这里存的是字符串,不是枚举,因为SharedPreferences不支持枚举类型。
_themeMode = ThemeMode.values.firstWhere(
(e) => e.toString() == 'ThemeMode.$themeModeString',
orElse: () => ThemeMode.system,
);
这行代码把字符串转回枚举。ThemeMode.values是所有枚举值的列表,firstWhere找到匹配的那个。为什么要比较**‘ThemeMode.$themeModeString’?因为枚举的toString()返回的是’ThemeMode.light’**这种格式。
orElse是找不到时的默认值,返回ThemeMode.system。虽然正常情况不会找不到,但加上这个参数更安全。
notifyListeners();
}
加载完要通知监听者,MaterialApp会收到通知,应用主题就更新了。
保存主题设置
用户切换主题时,要保存到本地:
Future<void> setThemeMode(ThemeMode mode) async {
_themeMode = mode;
先更新内存中的值,UI会立即响应。
final prefs = await SharedPreferences.getInstance();
await prefs.setString('themeMode', mode.toString().split('.').last);
然后保存到本地。mode.toString()返回’ThemeMode.light’,用split(‘.’)分割后取最后一部分,得到’light’。这样存储的字符串更简洁。
notifyListeners();
}
最后通知监听者。虽然前面已经更新了_themeMode,但还是要调用notifyListeners,确保所有监听者都收到通知。
注意这里用了await,确保数据真的写入了。如果不await,应用崩溃时数据可能还没保存,用户的设置就丢了。
浅色主题的定义
Flutter的主题系统很强大,可以定制各种细节。先看浅色主题:
ThemeData get lightTheme => ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
useMaterial3: true启用Material Design 3,这是最新的设计规范,组件更圆润、动画更流畅。
brightness: Brightness.light明确指定这是浅色主题。虽然colorScheme里也指定了,但这里再指定一次更保险。
ColorScheme.fromSeed是Material 3的新特性,从一个种子颜色生成整套配色方案。seedColor: Colors.blue表示主色调是蓝色,系统会自动生成primary、secondary、tertiary等一系列颜色,而且保证它们搭配和谐。
这比手动指定每个颜色方便多了,而且生成的配色方案符合Material Design规范,不会出现颜色不搭的情况。
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
AppBarTheme定制AppBar的样式。centerTitle: true让标题居中,这是iOS的习惯,Android默认是左对齐。咱们选择居中,看起来更平衡。
elevation: 0去掉AppBar的阴影。Material 3推荐扁平化设计,不要太多阴影。而且没有阴影,AppBar和body的分界更清晰。
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
CardTheme定制Card的样式。elevation: 2给Card加一点点阴影,让它有层次感,但不会太明显。
shape定义Card的形状。RoundedRectangleBorder是圆角矩形,borderRadius: 12表示圆角半径12。这个圆角大小刚刚好,不会太圆也不会太方。
深色主题的定义
深色主题和浅色主题结构一样,只是brightness不同:
ThemeData get darkTheme => ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
);
注意seedColor还是Colors.blue,和浅色主题一样。这样两个主题的色调是一致的,只是明暗不同。用户切换主题时,不会觉得换了个应用。
ColorScheme.fromSeed会根据brightness自动调整颜色。浅色主题的primary是亮蓝色,深色主题的primary会自动变成暗蓝色,而且保证在黑色背景上看得清楚。
AppBar和Card的配置和浅色主题完全一样,保持一致性。
在MaterialApp中使用主题
有了ThemeProvider,怎么让整个应用使用它?在main.dart里配置:
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return MaterialApp(
theme: themeProvider.lightTheme,
darkTheme: themeProvider.darkTheme,
themeMode: themeProvider.themeMode,
home: const MainScreen(),
);
},
),
);
}
}
ChangeNotifierProvider创建ThemeProvider实例,整个应用都能访问它。
Consumer监听ThemeProvider的变化。用户切换主题时,Consumer会重建,MaterialApp就会用新的主题。
MaterialApp的三个主题参数:
- theme是浅色主题,用户选择浅色或系统是浅色时使用
- darkTheme是深色主题,用户选择深色或系统是深色时使用
- themeMode决定用哪个主题
这三个参数配合,就能实现完整的主题切换功能。
主题切换的UI实现
在个人中心或设置页,加个主题切换开关:
Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return SwitchListTile(
secondary: Icon(
themeProvider.themeMode == ThemeMode.dark
? Icons.dark_mode
: Icons.light_mode,
),
title: const Text('深色模式'),
value: themeProvider.themeMode == ThemeMode.dark,
onChanged: (value) {
themeProvider.setThemeMode(
value ? ThemeMode.dark : ThemeMode.light,
);
},
);
},
)
这是个简单的开关,只能在浅色和深色之间切换。如果要支持"跟随系统",需要用对话框:
ListTile(
leading: const Icon(Icons.palette_outlined),
title: const Text('主题模式'),
subtitle: Text(_getThemeModeText(themeProvider.themeMode)),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('选择主题'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<ThemeMode>(
title: const Text('浅色'),
value: ThemeMode.light,
groupValue: themeProvider.themeMode,
onChanged: (value) {
if (value != null) {
themeProvider.setThemeMode(value);
Navigator.pop(context);
}
},
),
RadioListTile<ThemeMode>(
title: const Text('深色'),
value: ThemeMode.dark,
groupValue: themeProvider.themeMode,
onChanged: (value) {
if (value != null) {
themeProvider.setThemeMode(value);
Navigator.pop(context);
}
},
),
RadioListTile<ThemeMode>(
title: const Text('跟随系统'),
value: ThemeMode.system,
groupValue: themeProvider.themeMode,
onChanged: (value) {
if (value != null) {
themeProvider.setThemeMode(value);
Navigator.pop(context);
}
},
),
],
),
),
);
},
)
对话框里用RadioListTile实现单选,三个选项对应三种主题模式。用户选择后自动关闭对话框,体验很流畅。
主题切换的动画效果
Flutter的主题切换默认有动画,但可以自定义。在MaterialApp里加个参数:
MaterialApp(
theme: themeProvider.lightTheme,
darkTheme: themeProvider.darkTheme,
themeMode: themeProvider.themeMode,
themeAnimationDuration: const Duration(milliseconds: 300),
themeAnimationCurve: Curves.easeInOut,
home: const MainScreen(),
)
themeAnimationDuration是动画时长,300毫秒刚刚好,不会太快也不会太慢。
themeAnimationCurve是动画曲线,Curves.easeInOut是先加速后减速,看起来很自然。
这样切换主题时,颜色会平滑过渡,不会突然变化。
适配不同组件的主题
有些组件需要根据主题调整样式。比如文字颜色:
Text(
'标题',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
)
**Theme.of(context)**获取当前主题,colorScheme.onSurface是表面上的文字颜色。浅色主题是黑色,深色主题是白色,自动适配。
不要硬编码颜色:
// 错误:硬编码黑色
Text('标题', style: TextStyle(color: Colors.black))
// 正确:使用主题颜色
Text('标题', style: TextStyle(color: Theme.of(context).colorScheme.onSurface))
硬编码的颜色在深色主题下可能看不清。
自定义颜色的适配
如果要用自定义颜色,也要适配主题:
Container(
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[800]
: Colors.grey[200],
)
根据brightness判断当前是深色还是浅色,然后用不同的颜色。
或者用ColorScheme的颜色:
Container(
color: Theme.of(context).colorScheme.surface,
)
surface是表面颜色,浅色主题是白色,深色主题是深灰色,自动适配。
图片的主题适配
有些图片在深色主题下看不清,需要换一张:
Image.asset(
Theme.of(context).brightness == Brightness.dark
? 'assets/logo_dark.png'
: 'assets/logo_light.png',
)
或者给图片加个颜色滤镜:
Image.asset(
'assets/logo.png',
color: Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
)
状态栏和导航栏的适配
Android的状态栏和导航栏也要适配主题:
import 'package:flutter/services.dart';
void _updateSystemUI(BuildContext context) {
final brightness = Theme.of(context).brightness;
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: Theme.of(context).colorScheme.surface,
systemNavigationBarIconBrightness: brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
),
);
}
statusBarColor设置为透明,让状态栏和AppBar融为一体。
statusBarIconBrightness控制状态栏图标的颜色。深色主题用浅色图标,浅色主题用深色图标,这样才看得清。
systemNavigationBarColor是导航栏的背景色,用主题的surface颜色。
systemNavigationBarIconBrightness是导航栏图标的颜色,和状态栏一样。
在MaterialApp的builder里调用:
MaterialApp(
builder: (context, child) {
_updateSystemUI(context);
return child!;
},
)
主题切换的性能优化
主题切换会重建整个应用,如果页面很复杂,可能会卡顿。可以用这些方法优化:
使用const构造函数:
const Text('标题') // 不会重建
Text('标题') // 会重建
const的Widget不会重建,性能更好。
缓存复杂的Widget:
class MyWidget extends StatelessWidget {
static final _cachedChild = ExpensiveWidget();
Widget build(BuildContext context) {
return Container(
child: _cachedChild,
);
}
}
如果某个Widget很复杂,可以缓存起来,避免重复创建。
使用RepaintBoundary:
RepaintBoundary(
child: ComplexWidget(),
)
RepaintBoundary会把子Widget单独渲染,主题切换时不会重绘它。
一些实用的扩展
主题系统还可以加很多实用功能。
自定义主题颜色:
让用户选择主色调:
class ThemeProvider extends ChangeNotifier {
Color _seedColor = Colors.blue;
void setSeedColor(Color color) {
_seedColor = color;
notifyListeners();
}
ThemeData get lightTheme => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: Brightness.light,
),
);
}
定时切换主题:
晚上自动切换到深色模式:
void _scheduleThemeSwitch() {
final now = DateTime.now();
final nightStart = DateTime(now.year, now.month, now.day, 22, 0);
final dayStart = DateTime(now.year, now.month, now.day, 7, 0);
if (now.hour >= 22 || now.hour < 7) {
setThemeMode(ThemeMode.dark);
} else {
setThemeMode(ThemeMode.light);
}
}
主题预览:
切换主题前先预览效果:
void _showThemePreview(ThemeMode mode) {
showDialog(
context: context,
builder: (context) => Theme(
data: mode == ThemeMode.dark ? darkTheme : lightTheme,
child: AlertDialog(
title: const Text('主题预览'),
content: const Text('这是预览效果'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
setThemeMode(mode);
Navigator.pop(context);
},
child: const Text('应用'),
),
],
),
),
);
}
常见问题
主题切换后状态丢失:可能是Widget被重建了。用AutomaticKeepAliveClientMixin保持状态:
class MyPage extends StatefulWidget {
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> with AutomaticKeepAliveClientMixin {
bool get wantKeepAlive => true;
Widget build(BuildContext context) {
super.build(context);
return Container();
}
}
某些颜色不适配主题:检查是否硬编码了颜色。改用Theme.of(context).colorScheme的颜色。
主题切换有闪烁:可能是动画时长太短。增加themeAnimationDuration到500毫秒。
写在最后
主题切换功能看起来简单,但要做好不容易。要考虑数据持久化、颜色适配、动画效果、性能优化。
最重要的是用户体验。切换要流畅,颜色要和谐,所有组件都要适配。这些细节做好了,用户才会喜欢用你的应用。
代码写完了,记得在不同场景下测试。白天切换到深色会不会太暗?晚上切换到浅色会不会太亮?跟随系统能不能正常工作?这些都要测试到。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在这里你可以找到更多Flutter开发资源,与其他开发者交流经验,共同进步。
更多推荐
所有评论(0)