跳转到正文
YGJoon 技术博客

Flutter 主题切换实现详解

发布于:
·1.2k 字 · 约 3 分钟·作者:
ThemeData 定义亮暗两套主题,ThemeMode 切 system/light/dark,ChangeNotifier 触发整树重建
Flutter 主题切换实现详解文章封面
目录

简介

主题切换是应用中常见的功能,Flutter 提供了完整的主题支持。本文介绍如何实现动态主题切换功能。

基本实现

定义主题数据

class AppTheme {
  static final ThemeData lightTheme = ThemeData(
    brightness: Brightness.light,
    primarySwatch: Colors.blue,
    scaffoldBackgroundColor: Colors.white,
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.blue,
      foregroundColor: Colors.white,
    ),
    textTheme: TextTheme(
      bodyLarge: TextStyle(color: Colors.black87),
      bodyMedium: TextStyle(color: Colors.black87),
    ),
  );

  static final ThemeData darkTheme = ThemeData(
    brightness: Brightness.dark,
    primarySwatch: Colors.blue,
    scaffoldBackgroundColor: Colors.grey[900],
    appBarTheme: AppBarTheme(
      backgroundColor: Colors.grey[850],
      foregroundColor: Colors.white,
    ),
    textTheme: TextTheme(
      bodyLarge: TextStyle(color: Colors.white),
      bodyMedium: TextStyle(color: Colors.white70),
    ),
  );
}

主题管理

class ThemeProvider extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.system;

  ThemeMode get themeMode => _themeMode;

  bool get isDarkMode => _themeMode == ThemeMode.dark;

  void setThemeMode(ThemeMode mode) {
    _themeMode = mode;
    notifyListeners();
  }

  void toggleTheme() {
    _themeMode = isDarkMode ? ThemeMode.light : ThemeMode.dark;
    notifyListeners();
  }
}

完整示例

// 主应用
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) => ThemeProvider(),
      child: Consumer<ThemeProvider>(
        builder: (context, themeProvider, child) {
          return MaterialApp(
            title: 'Theme Demo',
            theme: AppTheme.lightTheme,
            darkTheme: AppTheme.darkTheme,
            themeMode: themeProvider.themeMode,
            home: HomePage(),
          );
        },
      ),
    );
  }
}

// 主页
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = Provider.of<ThemeProvider>(context);
    
    return Scaffold(
      appBar: AppBar(
        title: Text('Theme Demo'),
        actions: [
          IconButton(
            icon: Icon(
              themeProvider.isDarkMode
                  ? Icons.light_mode
                  : Icons.dark_mode,
            ),
            onPressed: () => themeProvider.toggleTheme(),
          ),
        ],
      ),
      body: ListView(
        padding: EdgeInsets.all(16),
        children: [
          Card(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Theme Settings',
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  SizedBox(height: 16),
                  RadioListTile<ThemeMode>(
                    title: Text('System'),
                    value: ThemeMode.system,
                    groupValue: themeProvider.themeMode,
                    onChanged: (value) {
                      themeProvider.setThemeMode(value!);
                    },
                  ),
                  RadioListTile<ThemeMode>(
                    title: Text('Light'),
                    value: ThemeMode.light,
                    groupValue: themeProvider.themeMode,
                    onChanged: (value) {
                      themeProvider.setThemeMode(value!);
                    },
                  ),
                  RadioListTile<ThemeMode>(
                    title: Text('Dark'),
                    value: ThemeMode.dark,
                    groupValue: themeProvider.themeMode,
                    onChanged: (value) {
                      themeProvider.setThemeMode(value!);
                    },
                  ),
                ],
              ),
            ),
          ),
          SizedBox(height: 16),
          Card(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Preview',
                    style: Theme.of(context).textTheme.titleLarge,
                  ),
                  SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: () {},
                    child: Text('Elevated Button'),
                  ),
                  SizedBox(height: 8),
                  OutlinedButton(
                    onPressed: () {},
                    child: Text('Outlined Button'),
                  ),
                  SizedBox(height: 8),
                  TextButton(
                    onPressed: () {},
                    child: Text('Text Button'),
                  ),
                  SizedBox(height: 16),
                  TextField(
                    decoration: InputDecoration(
                      labelText: 'Input Field',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

主题持久化

保存主题设置

class ThemePreferences {
  static const String key = 'theme_mode';
  
  static Future<void> saveThemeMode(ThemeMode mode) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(key, mode.toString());
  }
  
  static Future<ThemeMode> getThemeMode() async {
    final prefs = await SharedPreferences.getInstance();
    final value = prefs.getString(key);
    return ThemeMode.values.firstWhere(
      (mode) => mode.toString() == value,
      orElse: () => ThemeMode.system,
    );
  }
}

使用持久化

class ThemeProvider extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.system;

  ThemeProvider() {
    _loadThemeMode();
  }

  Future<void> _loadThemeMode() async {
    _themeMode = await ThemePreferences.getThemeMode();
    notifyListeners();
  }

  Future<void> setThemeMode(ThemeMode mode) async {
    _themeMode = mode;
    await ThemePreferences.saveThemeMode(mode);
    notifyListeners();
  }
}

最佳实践

  1. ThemeData 做成 static final 常量(文中 AppTheme 已是这个做法),别在 build 里 new:每次 build 都 new 一个 ThemeData 会触发整树不必要的重建,切个主题掉几帧。
  2. 颜色、字号一律走 Theme.of(context),别在业务组件里硬编码 Colors.black/Colors.white:硬编码的黑色文字在深色模式下直接看不见,切主题等于白切。
  3. 持久化读主题是异步的,启动时会闪一下默认主题:在 MaterialApp 外包一层启动屏/FutureBuilder,等 SharedPreferences 读完再 build——别让用户看到”白屏一闪变黑”的廉价感。

注意事项

  1. 切主题必然触发整树 rebuild,这是正常的:别为了”性能”去 hack 局部不刷新;真觉得切换生硬,用 AnimatedTheme 给 200~300ms 过渡动画,体验反而更好。
  2. 第三方组件不跟 ThemeData 走:WebView、地图、图表库的配色要单独适配——上线前把第三方页面逐个切主题看一遍,别以为切了主题就全黑了。
  3. 深色模式不是”背景变纯黑”就行:纯黑 #000000 在 OLED 上省电但对比刺眼,Material 规范推荐深灰(如 #121212);正文用 white70 而非纯白,长时间看不累眼。
  4. 浅色 logo、插图要准备深色版:白底图片在深色背景上会糊成一块补丁,视觉上比”没做深色模式”更廉价——切主题时顺手检查一遍图片资源。

实战案例:GetX 一行切主题的代价

我手里的几个 GetX 项目(电商、物流类 App,统一用的 get ^4.7.2)切主题根本不用写 Provider:Get.changeThemeMode() 一行搞定,Get.isDarkMode 随处可读,比官方 themeMode + ChangeNotifier 那套模板代码少写一个文件。当时选 GetX 就是图这个”顺手”。但代价是隐性的:主题状态和 GetX 强耦合之后,万一哪天团队决定换状态管理(比如切 Riverpod),所有 Get.changeTheme 的调用点都得翻出来重写;而官方写法啰嗦归啰嗦,主题逻辑和状态管理是解耦的。我的取舍结论是:如果项目已经全家桶用了 GetX(路由、依赖注入都在用),那就别扭捏,直接用它的主题 API;如果只是”为了切主题引入 GetX”,那就本末倒置了,老老实实按官方文档写。技术选型最怕的就是”只用它 10% 的功能,却背了 100% 的耦合”。

总结

通过合理使用 Flutter 的主题系统和状态管理,可以轻松实现动态主题切换功能。注意在实现过程中要考虑性能优化和用户体验。


分享这篇文章:


相关文章

上一篇
Flutter SharedPreferences 详解
下一篇
Flutter 文件操作详解