Flutter之旅 -- 主题切换
本篇文章主要介绍以下几个内容:
- Flutter 主题系统的底层渲染原理和 Widget 树重建机制
- 与 Android/iOS 原生主题系统的对比
- 使用 Riverpod 实现主题切换的代码示例

本文实现效果:

1. Flutter主题系统底层原理
注:以下涉及到的源码部分是 Flutter 3.24.3 中的简化版本。
1.1 Widget 树与主题传播机制
想象一下,Flutter 的主题系统就像一个大家族的遗传基因一样。
在这个家族中,每个 Widget
都是家族成员,而主题数据就像是家族的"遗传基因",从祖先传递给后代,像家族遗传一样的主题传递。
- 主题的"家族遗传"原理
// 当我们调用 Theme.of(context) 时,就像在问:"我的家族基因是什么?"
static ThemeData of(BuildContext context) {
// Flutter会沿着家族树向上查找,找到最近的"主题祖先"
final _InheritedTheme? inheritedTheme =
context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
// 如果找到了主题祖先,就继承它的"基因"(主题数据)
final ThemeData theme = inheritedTheme?.theme ?? ThemeData.fallback();
return theme;
}
- 主题传播就像"家族通知系统"
当在 Widget
中调用 Theme.of(context)
时,就像在家族群里问:"咱家现在流行什么风格?"
// 实际的"家族通讯录"代码 - Element类的核心方法
abstract class Element extends DiagnosticableTree implements BuildContext {
Map<Type, InheritedElement>? _inheritedWidgets; // 家族通讯录
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
// 1. 先查通讯录:我之前联系过这个主题祖先吗?
final InheritedElement? ancestor = _inheritedWidgets?[T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T?; // 直接联系
}
// 2. 没联系过,就向上找最近的主题祖先
_inheritedWidgets ??= HashMap<Type, InheritedElement>();
final InheritedElement? ancestor = getElementForInheritedWidgetOfExactType<T>();
if (ancestor != null) {
_inheritedWidgets![T] = ancestor; // 3. 把联系方式存到通讯录
return dependOnInheritedElement(ancestor, aspect: aspect) as T?; // 4. 建立订阅关系
}
return null; // 找不到主题祖先
}
}
举个例子:
class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 就像问家族群:"现在流行什么颜色?"
final theme = Theme.of(context);
return Container(
// 按照家族风格来装扮自己
color: theme.primaryColor,
child: Text('按钮', style: theme.textTheme.labelLarge),
);
}
}
这种机制确保了:
- 🎯 精准传递: 每个
Widget
都能获得正确的主题信息; - ⚡ 高效更新: 只有真正关心主题的
Widget
才会收到变化通知; - 🔄 自动同步: 主题一变化,所有相关
Widget
都会自动更新。
1.2 主题数据的内存结构
ThemeData 就像一家高端定制服装店,每套服装(主题)都是完整的成品套装,包含100多件配套单品(颜色、字体、样式等)。
这家店非常聪明,采用了"成品套装"和"共享配件"的策略来节省成本和空间。
- ThemeData 的"智能衣柜"结构
Flutter 中的 ThemeData 实际上是一个不可变对象,包含100多个 final
字段:
// 这是概念示例,用于理解ThemeData的优化策略
@immutable
class ThemeData {
// 实际上ThemeData的所有字段都是final的
final ColorScheme colorScheme; // 颜色搭配
final TextTheme textTheme; // 字体风格
final ButtonThemeData buttonTheme; // 按钮样式
final AppBarTheme appBarTheme; // 顶部栏样式
// ... 还有100多个final字段
const ThemeData.raw({
required this.colorScheme,
required this.textTheme,
required this.buttonTheme,
required this.appBarTheme,
// ... 所有字段都在构造时确定
});
// 写时复制的核心:copyWith方法
ThemeData copyWith({
ColorScheme? colorScheme,
TextTheme? textTheme,
ButtonThemeData? buttonTheme,
}) {
return ThemeData.raw(
// 只有指定的字段使用新值,其他字段复用原对象的引用
colorScheme: colorScheme ?? this.colorScheme,
textTheme: textTheme ?? this.textTheme,
buttonTheme: buttonTheme ?? this.buttonTheme,
appBarTheme: this.appBarTheme, // 复用原引用
// ... 其他未变更的字段都复用原引用
);
}
}
- 所有字段都是
final
,确保不可变性; -
copyWith
方法实现写时复制,未变更的字段共享原对象引用; - 工厂构造函数处理复杂的默认值计算和依赖关系。
- 高端定制服装店的经营秘诀
-
成品套装 🎽: 每套主题都是完整的成品,所有配件(
final
字段)在制作时就固定好了; - 共享配件 👔: 相同的配件(如相同的颜色、字体)在不同套装间共享,不重复制作;
- 定制服务 ✂️: 客户要改款式时,只重新制作需要改的配件,其他配件继续用原来的;
- 工厂预制 🏭: 复杂的搭配和计算在工厂(构造函数)里完成,客户拿到的是成品。
// 举个例子:
final 商务套装 = ThemeData.light(); // 制作一套完整的商务套装
final 商务套装换领带 = 商务套装.copyWith(primaryColor: Colors.blue); // 只换领带颜色
// 神奇的是:商务套装和商务套装换领带共享了99%的配件(西装、衬衫、鞋子等)
// 只有领带是新制作的,大大节省了材料和成本! ✨
- 总部的"换装通知"
想象一下,一个连锁品牌决定更换店面装修风格。
总部不需要让所有店铺都重新装修,只需要通知那些真正需要更新装修的店铺。
Flutter 的主题切换就是这样一个智能的"连锁管理系统"!
当主题发生变化时,Flutter 就像连锁品牌的总部发出换装通知:
// InheritedWidget 的核心方法 -- 总部检查:"这次的装修风格和上次有什么不同吗?"
class _InheritedTheme extends InheritedTheme {
const _InheritedTheme({
required this.theme,
required super.child,
});
final ThemeData theme;
@override
bool updateShouldNotify(_InheritedTheme old) {
// 如果装修风格真的变了,就发通知
return theme != old.theme; // 就像对比两套装修方案
}
}
智能换装的三步走
-
🏢 检查变化: "装修风格真的变了吗?"
- 如果没变化,就不发通知,各店铺继续正常营业
- 如果变了,才开始下一步
-
📋 查看加盟店名单: "哪些店铺需要更新装修?"
- Flutter 维护一个"订阅列表",记录哪些
Widget
关心主题变化 - 就像连锁总部的加盟店名单,只通知相关的店铺
- Flutter 维护一个"订阅列表",记录哪些
-
📢 精准通知: "只通知需要换装的店铺"
- 给列表中的每个
Widget
发送"主题更新"通知 - 收到通知的
Widget
会重新"装修"(重建)
- 给列表中的每个
为什么这样做这么高效?
// 举个连锁店的例子
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
Text('我是独立小店'), // 🏪 不需要换装
Container(
color: Theme.of(context).primaryColor, // 🏢 连锁店,需要换装
child: Text('我是连锁店分店'),
),
Image.asset('logo.png'), // 🏪 独立小店,不受影响
],
);
}
}
当总部发出"从亮色装修改为暗色装修"的通知时:
- ✅ 只有连锁店(使用了
Theme.of(context)
的Container
)会重新装修 - ❌ 独立小店继续正常营业,不受影响
- 🚀 结果:换装效率高,用户体验流畅!
这套连锁管理机制的优势
- 🎯 精准管理: 只更新需要更新的连锁店
- ⚡ 高效执行: 避免不必要的装修,节省成本
- 🧠 智能识别: 自动区分连锁店和独立店
- 💚 资源节约: 减少不必要的工作量,提高效率
这就是为什么 Flutter 的主题切换能做到既快速又流畅的秘密!
2. 跨平台主题系统对比
2.1 Android 原生 vs Flutter 主题
Android 主题机制
<!-- Android themes.xml -->
<style name="AppTheme" parent="Theme.Material3.DayNight">
<item name="colorPrimary">@color/primary</item>
<item name="colorOnPrimary">@color/on_primary</item>
<item name="android:windowBackground">@color/background</item>
</style>
<!-- 夜间模式 themes.xml (night) -->
<style name="AppTheme" parent="Theme.Material3.DayNight">
<item name="colorPrimary">@color/primary_dark</item>
<item name="colorOnPrimary">@color/on_primary_dark</item>
</style>
Android主题特点:
- 基于资源限定符的静态切换
- 系统级别的配置变更处理
-
Activity
重建机制 - 硬件加速的渲染优化
Flutter vs Android 对比
特性 | Android原生 | Flutter |
---|---|---|
主题切换机制 | Configuration Change + Activity 重建 | Widget 树重建 |
性能开销 | Activity完全重建,开销较大 | 智能局部重建,开销较小 |
动画支持 | 系统级转场动画 | 自定义动画,更灵活 |
内存管理 | 系统自动管理 | 开发者可控 |
跨平台一致性 | 平台特定 | 完全一致 |
自定义能力 | 受系统限制 | 完全自定义 |
2.2 iOS 原生主题
iOS 主题实现
// iOS 13+ 系统主题支持
class ThemeManager {
static func applyTheme() {
if #available(iOS 13.0, *) {
// 使用系统提供的动态颜色
view.backgroundColor = UIColor.systemBackground
label.textColor = UIColor.label
} else {
// 手动管理主题
view.backgroundColor = isDarkMode ? .black : .white
}
}
}
// 监听系统主题变化
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
applyTheme()
}
}
关键差异:
- iOS 依赖
UITraitCollection
系统 - Flutter 使用
MediaQuery.platformBrightness
- iOS 有系统级动画支持
- Flutter 需要自实现切换动画
3. 使用 Riverpod 实现主题切换
3.1 定义颜色常量
首先定义应用中使用的所有颜色常量,分别为亮色和暗色主题:
// lib/common/theme/color.dart
import 'package:flutter/material.dart';
// 主要颜色
const primaryColor = Color(0xFF1880FF);
const PrimaryColorDark = Color(0xFF3791FF);
// 背景颜色
const bgColor = Color(0xFFFFFFFF);
const bgColorDark = Color(0xFF191919);
// 文字颜色
const textPrimaryColor = Color(0xFF1C1D1F);
const textPrimaryColorDark = Color(0xFFFFFFFF);
const textSecondaryColor = Color(0xFF444444);
const textSecondaryColorDark = Color(0xFFA8A8A8);
3.2 创建主题配置
定义亮色和暗色两套完整的主题配置:
// lib/common/theme/theme.dart
import 'package:flutter/material.dart';
import 'color.dart';
/// 亮色主题
ThemeData lightTheme(String fontFamily) =>
ThemeData.light(useMaterial3: true).copyWith(
platform: TargetPlatform.iOS,
primaryColor: primaryColor,
textTheme: textTheme(fontFamily),
appBarTheme: const AppBarTheme(
backgroundColor: bgColor,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
fontSize: 16,
height: 1.5,
color: textPrimaryColor,
fontWeight: FontWeight.w500,
),
),
dialogTheme: DialogThemeData(
backgroundColor: bgColor,
titleTextStyle: textTheme(fontFamily).titleMedium,
contentTextStyle: textTheme(fontFamily)
.bodyMedium
?.copyWith(color: textSecondaryColor),
),
// 更多亮色主题配置...
);
/// 暗色主题
ThemeData darkTheme(String fontFamily) =>
ThemeData.dark(useMaterial3: true).copyWith(
platform: TargetPlatform.iOS,
primaryColor: PrimaryColorDark,
textTheme: textDarkTheme(fontFamily),
appBarTheme: const AppBarTheme(
backgroundColor: bgColorDark,
elevation: 0,
centerTitle: true,
titleTextStyle: TextStyle(
fontSize: 16,
height: 1.5,
color: textPrimaryColorDark,
fontWeight: FontWeight.w500,
),
),
dialogTheme: DialogThemeData(
backgroundColor: bgColorDark,
titleTextStyle: textDarkTheme(fontFamily).titleMedium,
contentTextStyle: textDarkTheme(fontFamily)
.bodyMedium
?.copyWith(color: textSecondaryColorDark),
),
// 更多暗色主题配置...
);
3.3 实现持久化存储
如使用 SharedPreferences
保存用户的主题选择:
// lib/common/utils/share_preferences_utils.dart
class SharePreferenceUtils {
/// 保存主题模式
static Future<bool> saveThemeMode(String themeMode) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return await prefs.setString(SharePreferenceKey.themeMode, themeMode);
}
/// 获取主题模式
static Future<String> getThemeMode() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString(SharePreferenceKey.themeMode) ??
ThemeModeConstant.system;
}
}
class SharePreferenceKey {
static const String themeMode = "THEME_MODE"; // 主题模式
}
// 主题模式常量
class ThemeModeConstant {
static const String system = "system";
static const String light = "light";
static const String dark = "dark";
}
3.4 创建主题状态管理器
使用 Riverpod 的 StateNotifier 创建主题状态管理器:
// lib/common/theme/theme_provider.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constant.dart';
import '../utils/share_preferences_utils.dart';
/// 主题模式
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
ThemeModeNotifier() : super(ThemeMode.system) {
_initThemeMode();
}
/// 初始化主题模式
Future<void> _initThemeMode() async {
final savedThemeMode = await SharePreferenceUtils.getThemeMode();
switch (savedThemeMode) {
case ThemeModeConstant.light:
state = ThemeMode.light;
break;
case ThemeModeConstant.dark:
state = ThemeMode.dark;
break;
default:
state = ThemeMode.system;
}
}
/// 设置主题模式
Future<void> setThemeMode(ThemeMode mode) async {
state = mode;
String modeString;
switch (mode) {
case ThemeMode.dark:
modeString = ThemeModeConstant.dark;
break;
case ThemeMode.light:
modeString = ThemeModeConstant.light;
break;
default:
modeString = ThemeModeConstant.system;
}
await SharePreferenceUtils.saveThemeMode(modeString);
}
/// 切换主题模式
Future<void> toggleThemeMode(bool isDark) async {
if (isDark) {
await setThemeMode(ThemeMode.dark);
} else {
await setThemeMode(ThemeMode.light);
}
}
/// 智能切换主题模式(无需传参)
/// 如果当前是暗黑模式 or 跟随系统且系统是暗黑模式,切换为亮色模式
/// 如果当前是亮色模式 or 跟随系统且系统是亮色模式,切换为暗黑模式
Future<void> toggleTheme() async {
// 获取系统当前主题模式
final brightness =
WidgetsBinding.instance.platformDispatcher.platformBrightness;
final isSystemDark = brightness == Brightness.dark;
// 根据当前模式智能切换
if (state == ThemeMode.dark) {
await setThemeMode(ThemeMode.light);
} else if (state == ThemeMode.system && isSystemDark) {
await setThemeMode(ThemeMode.light);
} else {
await setThemeMode(ThemeMode.dark);
}
}
}
/// 主题模式状态提供者
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
(ref) => ThemeModeNotifier(),
);
/// 系统主题是否为暗黑模式的提供者
final isSystemDarkModeProvider = Provider<bool>((ref) {
final brightness =
WidgetsBinding.instance.platformDispatcher.platformBrightness;
return brightness == Brightness.dark;
});
/// 主题是否为暗黑模式的提供者
final isDarkModeProvider = Provider<bool>((ref) {
final themeMode = ref.watch(themeModeProvider);
final isDarkSystem = ref.watch(isSystemDarkModeProvider);
if (themeMode == ThemeMode.dark) {
return true; // 如果是暗黑模式,开关打开
} else if (themeMode == ThemeMode.light) {
return false; // 如果是亮色模式,开关关闭
} else {
return isDarkSystem; // 如果是跟随系统,则根据系统当前主题决定
}
});
3.5 在 MaterialApp 中应用主题
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'common/theme/theme.dart';
import 'common/theme/theme_provider.dart';
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeProvider);
return MaterialApp(
title: 'Flutter主题切换示例',
theme: lightTheme(), // 亮色主题
darkTheme: darkTheme(), // 暗色主题
themeMode: themeMode, // 当前主题模式
home: MyHomePage(),
);
}
}
3.6 在UI中使用主题
有了主题系统,让界面根据主题变化就像变脸一样简单:
// 方式一:直接使用主题数据(推荐)
class MyCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // 获取当前主题
return Card(
color: theme.colorScheme.surface, // 自动适配的背景色
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Text(
'标题',
style: theme.textTheme.headlineSmall, // 自动适配的标题样式
),
Text(
'内容描述',
style: theme.textTheme.bodyMedium, // 自动适配的正文样式
),
ElevatedButton(
onPressed: () {},
child: Text('按钮'), // 按钮会自动使用主题色
),
],
),
),
);
}
}
// 方式二:手动判断主题模式(适用于特殊情况)
class CustomWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDarkMode = ref.watch(isDarkModeProvider);
return Container(
// 根据主题模式选择不同的背景色
color: isDarkMode ? bgPrimaryColorDark : bgColor,
child: Column(
children: [
Icon(
Icons.star,
// 暗色模式用金色,亮色模式用蓝色
color: isDarkMode ? Colors.amber : Colors.blue,
),
Text(
'自定义样式文字',
style: TextStyle(
color: isDarkMode ? textPrimaryColorDark : textPrimaryColor,
fontSize: 16,
),
),
],
),
);
}
}
// 主题切换开关
class ThemeSwitchWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDarkMode = ref.watch(isDarkModeProvider);
return Switch(
value: isDarkMode,
onChanged: (value) {
// 切换主题
ref.read(themeModeProvider.notifier).toggleTheme();
},
);
}
}
使用建议:
- ✅ 优先使用方式一: 让 Flutter 自动处理主题适配,代码更简洁;
- ⚠️ 谨慎使用方式二: 只在需要特殊定制时使用,避免代码冗余;
- 🎨 保持一致性: 同类型的组件使用相同的主题适配方式。
4. 小结
4.1 核心原理
-
InheritedWidget 机制: Flutter 主题系统基于
InheritedWidget
实现,像家族遗传基因一样,通过依赖追踪和智能通知机制,确保只有真正依赖主题数据的Widget
才会重建。 -
不可变对象设计: ThemeData 是不可变对象,所有字段都是
final
。采用写时复制策略,像 "高端定制服装店" 一样,只有变更的配件才重新制作,其他配件共享引用,最大化内存使用效率。 -
智能重建机制: 主题切换时采用 "连锁店管理" 模式,只重建真正依赖主题数据的
Widget
,实现高效的界面更新。 -
跨平台优势: 相比 Android 和 iOS 原生主题系统,Flutter 提供了更灵活的自定义能力和更好的跨平台一致性。
4.2 实现要点
-
状态管理: 使用 Riverpod 的
StateNotifier
管理主题状态,提供类型安全和自动资源管理。 -
持久化存储: 通过
SharedPreferences
保存用户主题偏好,确保应用重启后主题设置不丢失。 -
智能切换: 实现了支持系统主题跟随的智能切换逻辑,提升用户体验。
-
注意事项: 主题数据的不可变性
- ThemeData是不可变对象,修改主题时必须使用
copyWith
方法; - 避免直接修改 ThemeData 的属性,这会导致不可预期的行为。
- ThemeData是不可变对象,修改主题时必须使用