Flutter 主题切换实战:系统适配+手动切换+持久化,15分钟落地暗黑模式

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 init() async {_prefs = await SharedPreferences.getInstance();

// 读取本地存储的主题模式(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 setThemeMode(ThemeModeType mode) async {if (_themeMode == mode) return; // 避免重复切换,提升性能

_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(context);

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(context);

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(title: const Text('跟随系统'),

subtitle: const Text('根据系统设置自动切换亮色 / 暗色'),

value: ThemeModeType.system,

groupValue: currentMode,

onChanged: (value) {if (value != null) {themeProvider.setThemeMode(value);

}

},

activeColor: Theme.of(context).primaryColor,

),

// 浅色模式选项

RadioListTile(title: const Text('浅色模式'),

subtitle: const Text('始终使用亮色主题,适合白天使用'),

value: ThemeModeType.light,

groupValue: currentMode,

onChanged: (value) {if (value != null) {themeProvider.setThemeMode(value);

}

},

activeColor: Theme.of(context).primaryColor,

),

// 暗色模式选项

RadioListTile(title: const Text('深色模式'),

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(context);

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(context) 在ChangeNotifierProvider的子树中使用,若在顶层使用,需添加listen: false;

硬编码颜色导致适配失败 :自定义组件时,不要硬编码颜色(如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 中的主色调,就能快速落地。

如果在实现过程中遇到问题,欢迎在评论区留言,我会第一时间回复——毕竟,实战中踩过的坑,都值得分享给更多开发者。

Back to top: