Flutter 主题切换实战教程:15 分钟实现系统适配、手动切换与本地持久化,含完整可复用代码、Provider 状态管理、避坑指南,助力开发者快速落地暗黑模式,提升应用体验与可访问性。
作为一名 Flutter 开发者,我经手的每款应用几乎都会加入主题切换功能。不是为了炫技,而是用户真的需要:深夜刷应用时的护眼需求、不同场景下的个性化偏好、对弱视用户的可访问性支持,甚至是细节处体现的产品质感,都离不开这套看似简单的功能。
今天就把我实战中总结的最简落地方案分享给大家,无需复杂逻辑,15 分钟就能实现「系统自动适配 + 手动切换 + 本地持久化」的完整主题功能,代码可直接复制复用,还会补充实战中踩过的坑,帮你少走弯路。
为什么一定要做 Flutter 主题切换?
在动手之前,我们先明确一个核心:主题切换不是“锦上添花”,而是当前移动应用的“基础标配”,尤其是暗黑模式,已经成为 iOS 和 Android 两大系统的原生支持功能。
系统级用户期待:现在大部分用户都会根据时间切换系统主题,若你的应用不能同步适配,会显得非常突兀,甚至影响用户留存;
个性化与用户粘性:允许用户手动选择主题,满足不同用户的视觉偏好,细节处提升产品好感度,间接增加用户活跃度;
可访问性合规:高对比度的暗黑主题的和亮色主题,能更好地适配弱视用户,符合移动应用的可访问性要求,避免潜在的合规风险;
技术细节体现:看似简单的主题切换,实则涉及状态管理、本地持久化、系统交互等多个知识点,能体现开发者对产品细节的把控能力。
实战准备:环境与依赖
首先创建一个新的 Flutter 项目(若已有项目,可直接跳过这一步),然后在 pubspec.yaml 中添加两个核心依赖,这两个依赖是实战中最稳定、最常用的组合,无需额外引入复杂框架。
dependencies:
flutter:
sdk: flutter
provider: ^6.1.1 # 状态管理核心,用于监听主题变化、同步更新 UI
shared_preferences: ^2.2.2 # 本地持久化,保存用户主题选择,重启应用不丢失
添加完成后,执行 flutter pub get 安装依赖。这里提醒一句:尽量使用我指定的依赖版本(或兼容版本),避免高版本出现 API 变更导致的报错——这是我实战中踩过的第一个小坑,分享给大家。
第一步:定义主题数据(亮色 + 暗色,可直接复用)
主题的核心是ThemeData,我们需要分别定义亮色主题和暗色主题,统一管理颜色、字体、图标等样式,避免后续修改时到处找代码。
在 lib 目录下新建 theme 文件夹,创建 app_theme.dart 文件,代码如下(已优化细节,适配不同组件的样式统一):
import 'package:flutter/material.dart';
class AppTheme {
// 亮色主题(默认)static final ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blue, // 主色调,可根据你的产品色修改
scaffoldBackgroundColor: Colors.grey[50], // 页面背景色,比纯白更柔和
appBarTheme: const AppBarTheme(
elevation: 0, // 取消 AppBar 阴影,更简洁
backgroundColor: Colors.blue,
foregroundColor: Colors.white, // 标题、图标颜色
titleTextStyle: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
textTheme: TextTheme(
// 标题文本样式
titleLarge: const TextStyle(color: Colors.black87, fontSize: 20, fontWeight: FontWeight.bold),
// 正文文本样式
bodyLarge: TextStyle(color: Colors.black87, fontSize: 16),
bodyMedium: TextStyle(color: Colors.black54, fontSize: 14),
// 辅助文本样式
labelMedium: TextStyle(color: Colors.black45, fontSize: 12),
),
iconTheme: const IconThemeData(color: Colors.black87, size: 24),
// 按钮样式统一,避免每个按钮单独设置
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
);
// 暗色主题(适配系统暗黑模式 + 手动切换)static final ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.blueGrey, // 暗色模式主色调,避免过亮刺眼
scaffoldBackgroundColor: Colors.grey[900], // 深色背景,护眼不压抑
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: Colors.grey[850],
foregroundColor: Colors.white,
titleTextStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
textTheme: TextTheme(titleLarge: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold),
bodyLarge: TextStyle(color: Colors.white70, fontSize: 16),
bodyMedium: TextStyle(color: Colors.white60, fontSize: 14),
labelMedium: TextStyle(color: Colors.white40, fontSize: 12),
),
iconTheme: const IconThemeData(color: Colors.white70, size: 24),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
);
}
这里有两个实战细节提醒:
亮色主题的背景色没有用纯白(Colors.white),而是用了Colors.grey[50],长时间阅读更护眼;
统一设置了elevatedButtonTheme,避免后续开发中每个按钮单独写样式,提升开发效率,也保证 UI 统一性。
如果你的应用需要支持多套主题色(比如红色、绿色),可以将颜色参数化,后续会在进阶部分补充具体实现。
第二步:核心实现:主题管理器(状态管理 + 持久化)
主题切换的核心的是“状态管理”和“持久化”:既要让 UI 实时响应主题变化,也要让用户的选择在重启应用后依然生效。这里我们用 ChangeNotifier+Provider 做状态管理,shared_preferences做本地持久化,逻辑简洁且稳定。
在 lib 目录下新建 providers 文件夹,创建 theme_provider.dart 文件,代码如下(含详细注释,关键步骤标红):
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../theme/app_theme.dart';
// 主题模式枚举,明确三种状态,避免混乱
enum ThemeModeType {
light, // 浅色模式
dark, // 深色模式
system, // 跟随系统(默认)}
class ThemeProvider extends ChangeNotifier {
ThemeModeType _themeMode = ThemeModeType.system; // 默认跟随系统
late SharedPreferences _prefs; // 用于本地持久化
// 初始化:从本地读取保存的主题模式,避免重启后重置
Future
// 读取本地存储的主题模式(key 为 themeMode,首次启动为 null)final String? savedMode = _prefs.getString('themeMode');
if (savedMode != null) {
// 转换为 ThemeModeType 枚举,找不到则默认跟随系统
_themeMode = ThemeModeType.values.firstWhere((e) => e.toString() == savedMode,
orElse: () => ThemeModeType.system,);
}
notifyListeners(); // 通知 UI 更新}
// 对外提供当前主题模式(只读)ThemeModeType get themeMode => _themeMode;
// 根据当前主题模式,获取实际要使用的 ThemeData(需传入 context,判断系统亮度)ThemeData getTheme(BuildContext context) {switch (_themeMode) {
case ThemeModeType.light:
return AppTheme.lightTheme;
case ThemeModeType.dark:
return AppTheme.darkTheme;
case ThemeModeType.system:
// 跟随系统亮度,获取当前系统的亮度模式
final brightness = MediaQuery.platformBrightnessOf(context);
return brightness == Brightness.dark ? AppTheme.darkTheme : AppTheme.lightTheme;
}
}
// 切换主题模式,并保存到本地
Future
_themeMode = mode;
// 保存到本地,key 为 themeMode,值为枚举的字符串形式
await _prefs.setString('themeMode', mode.toString());
notifyListeners(); // 通知 UI 更新,同步切换主题}
// 判断当前是否为暗黑模式(对外提供,用于 UI 特殊适配)bool isDarkMode(BuildContext context) {final theme = getTheme(context);
return theme.brightness == Brightness.dark;
}
}
这里有一个关键避坑点:getTheme方法和 isDarkMode 方法都需要传入 BuildContext,因为要通过MediaQuery.platformBrightnessOf(context) 获取系统的亮度模式,没有上下文会报错。
另外,init方法是异步的,必须在应用启动前完成初始化,否则会出现“短暂显示默认主题,再切换到保存主题”的闪烁问题,后续会在 main.dart 中解决这个问题。
第三步:顶层注入 Provider,确保全局可用
主题管理器需要在整个应用中全局可用,因此我们需要在 main.dart 中,将 ThemeProvider 注入到应用顶层,同时确保初始化完成后再启动应用,避免闪烁。
修改 main.dart 代码如下:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/theme_provider.dart';
import 'screens/home_screen.dart';
import 'screens/settings_screen.dart';
void main() async {
// 确保 Flutter 绑定完成,否则 SharedPreferences 初始化会报错
WidgetsFlutterBinding.ensureInitialized();
// 初始化 ThemeProvider,读取本地保存的主题设置
final themeProvider = ThemeProvider();
await themeProvider.init();
runApp(
// 使用 MultiProvider,方便后续添加其他 Provider(如用户信息、全局状态)MultiProvider(
providers: [
// 注入 ThemeProvider,使用 value 方式,避免重复创建实例
ChangeNotifierProvider.value(value: themeProvider),
],
child: const MyApp(),),
);
}
class MyApp extends StatelessWidget {const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 监听 ThemeProvider 的变化,主题切换时自动重建 UI
final themeProvider = Provider.of
return MaterialApp(
title: 'Flutter 主题切换实战',
// 关键:使用 Flutter 官方推荐的方式,适配系统主题
theme: AppTheme.lightTheme, // 亮色主题默认值
darkTheme: AppTheme.darkTheme, // 暗色主题默认值
// 映射主题模式:将我们自定义的 ThemeModeType 转换为 Flutter 原生的 ThemeMode
themeMode: themeProvider.themeMode == ThemeModeType.system
? ThemeMode.system
: themeProvider.themeMode == ThemeModeType.light
? ThemeMode.light
: ThemeMode.dark,
// 路由管理,添加首页和设置页(主题切换在设置页)routes: {'/': (context) => const HomeScreen(),
'/settings': (context) => const SettingsScreen(),},
);
}
}
这里有两个实战优化点:
在 main 函数中先初始化ThemeProvider,再启动应用,彻底解决主题闪烁问题;
使用MultiProvider,方便后续添加其他全局状态(如用户信息、网络状态),无需修改顶层结构;
使用路由管理,将首页和设置页分开,结构更清晰,也符合实际开发习惯。
第四步:实现主题切换界面(用户可手动选择)
接下来创建设置页面,让用户可以手动选择“跟随系统”“浅色模式”“深色模式”,同时添加实时预览,让用户直观看到主题变化效果。
在 lib 目录下新建 screens 文件夹,创建 settings_screen.dart 文件,代码如下(UI 简洁美观,适配主题切换):
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/theme_provider.dart';
class SettingsScreen extends StatelessWidget {const SettingsScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {final themeProvider = Provider.of
final currentMode = themeProvider.themeMode;
return Scaffold(
appBar: AppBar(title: const Text('设置'),
leading: IconButton(icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
),
body: ListView(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20),
children: [
// 主题模式标题
const Text(
'主题模式',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
// 跟随系统选项
RadioListTile
subtitle: const Text('根据系统设置自动切换亮色 / 暗色'),
value: ThemeModeType.system,
groupValue: currentMode,
onChanged: (value) {if (value != null) {themeProvider.setThemeMode(value);
}
},
activeColor: Theme.of(context).primaryColor,
),
// 浅色模式选项
RadioListTile
subtitle: const Text('始终使用亮色主题,适合白天使用'),
value: ThemeModeType.light,
groupValue: currentMode,
onChanged: (value) {if (value != null) {themeProvider.setThemeMode(value);
}
},
activeColor: Theme.of(context).primaryColor,
),
// 暗色模式选项
RadioListTile
subtitle: const Text('始终使用暗色主题,适合夜间使用'),
value: ThemeModeType.dark,
groupValue: currentMode,
onChanged: (value) {if (value != null) {themeProvider.setThemeMode(value);
}
},
activeColor: Theme.of(context).primaryColor,
),
const SizedBox(height: 30),
const Divider(),
const SizedBox(height: 30),
// 主题实时预览卡片
Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'实时预览',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
const SizedBox(height: 20),
// 预览内容:模拟应用中的常见组件
Row(
children: [
Icon(themeProvider.isDarkMode(context)
? Icons.dark_mode
: Icons.light_mode,
size: 28,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'当前主题',
style: Theme.of(context).textTheme.bodyLarge,
),
Text(themeProvider.isDarkMode(context) ? '深色模式' : '浅色模式',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
],
),
const SizedBox(height: 20),
// 预览按钮和文本
ElevatedButton(onPressed: () {},
child: const Text('预览按钮'),
),
const SizedBox(height: 16),
Text(
'这是一段预览文本,用于展示主题切换后的文字颜色效果。',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
],
),
);
}
}
同时,创建首页home_screen.dart,添加一个跳转到设置页的按钮,方便用户进入主题设置:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/theme_provider.dart';
import 'settings_screen.dart';
class HomeScreen extends StatelessWidget {const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {final themeProvider = Provider.of
return Scaffold(
appBar: AppBar(title: const Text('首页'),
actions: [
// 跳转到设置页的按钮
IconButton(icon: const Icon(Icons.settings),
onPressed: () => Navigator.pushNamed(context, '/settings'),
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('当前主题:${themeProvider.isDarkMode(context) ?' 深色模式 ':' 浅色模式 '}',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 20),
ElevatedButton(onPressed: () => Navigator.pushNamed(context, '/settings'),
child: const Text('进入设置,切换主题'),
),
],
),
),
);
}
}
到这里,核心功能已经实现:用户可以在设置页切换主题,切换后实时预览效果,重启应用后依然保留上次的选择,系统切换主题时,应用也会自动同步。
实战避坑指南(必看)
这部分是我在多次实战中总结的问题,很多开发者都会踩坑,提前规避能节省大量时间:
热重载不生效 :修改ThemeData 后,若热重载不生效,不要纠结,直接重启应用即可——这是 Flutter 主题热重载的一个小 bug,目前暂无更好的解决办法;
主题闪烁问题 :必须在main 函数中先初始化 ThemeProvider,再启动应用,否则会出现“默认主题→保存主题”的闪烁,本文的main.dart 已经规避了这个问题;
Provider 访问报错 :确保Provider.of
硬编码颜色导致适配失败 :自定义组件时,不要硬编码颜色(如Colors.white、Colors.black),尽量使用Theme.of(context).colorScheme 或Theme.of(context).textTheme,例如:背景色用colorScheme.surface,文字用colorScheme.onSurface,这样才能自动适配主题切换;
系统主题切换无响应 :无需额外写监听代码,只要在MaterialApp 中设置了themeMode: ThemeMode.system,Flutter 会自动监听系统主题变化,同步更新应用主题。
进阶:支持多套主题色(可选)
如果你的应用需要支持多种主题色(如红色、绿色、蓝色),可以基于上面的代码进行扩展,核心思路是将颜色参数化,动态生成ThemeData。
简单实现步骤:
在 ThemeProvider 中添加主题色字段,如Color _primaryColor = Colors.blue;;
定义多套主题色选项(如红色、绿色),提供切换方法setPrimaryColor,并持久化保存;
修改 getTheme 方法,根据当前主题色和主题模式,动态生成 ThemeData,可使用ThemeData.copyWith 或ThemeData.from方法。
具体代码可根据你的产品需求调整,核心逻辑和本文的主题切换一致,无需额外引入新框架。
完整目录结构(规范开发)
最后,给大家展示规范的目录结构,方便大家在实际项目中复用:
lib/
├── main.dart # 应用入口,注入 Provider
├── providers/ # 状态管理文件夹
│ └── theme_provider.dart # 主题管理器
├── screens/ # 页面文件夹
│ ├── home_screen.dart # 首页
│ └── settings_screen.dart # 设置页(主题切换)└── theme/ # 主题配置文件夹
└── app_theme.dart # 亮色 / 暗色主题定义
总结
Flutter 的主题切换其实非常简单,核心就是三步:定义主题数据、管理主题状态、全局注入并适配 UI。整个核心代码不到 100 行,却能实现“系统适配 + 手动切换 + 本地持久化”的完整功能,极大提升用户体验。
暗黑模式已经不是高端应用的专属,而是每个 Flutter 开发者都应该掌握的基础功能。本文的代码可直接复制到项目中使用,只需根据你的产品色修改 AppTheme 中的主色调,就能快速落地。
如果在实现过程中遇到问题,欢迎在评论区留言,我会第一时间回复——毕竟,实战中踩过的坑,都值得分享给更多开发者。