【Flutter for OpenHarmony】Flutter 深色模式与主题切换的鸿蒙化适配与实战指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net


一、为什么我要做深色模式?

我是 IntMainJhy,上海某高校大一计算机专业的学生。说起深色模式,我真的有一段"真香"经历。

一开始我觉得深色模式就是"把背景改成黑色"嘛,有什么难的?结果做出来之后,我发现问题一大堆:

  • 深色模式下文字看不清楚
  • 按钮颜色和背景颜色撞色
  • 切换主题时页面闪烁
  • 有些组件完全没有响应主题变化

后来我花了整整两天时间研究 Flutter 的主题系统,才终于把深色模式做好。今天这篇文章,我就把我是怎么做的,全部分享出来。


二、深色模式基础

2.1 Flutter 主题系统

Flutter 的主题系统由两部分组成:

组件 说明
ThemeData 定义主题数据(颜色、字体、样式等)
Theme Widget,继承主题数据给子组件

2.2 主题模式

Flutter 支持三种主题模式:

模式 说明
ThemeMode.system 跟随系统设置
ThemeMode.light 始终浅色模式
ThemeMode.dark 始终深色模式

三、完整主题配置

3.1 颜色系统

// lib/mental_health/theme/app_colors.dart

import 'package:flutter/material.dart';

/// 心理健康 App 颜色系统
class AppColors {
  // ==================== 主色 ====================
  
  /// 主色调 - 蓝紫色
  static const Color primary = Color(0xFF6C63FF);
  
  /// 主色深色
  static const Color primaryDark = Color(0xFF5849BE);
  
  /// 主色浅色
  static const Color primaryLight = Color(0xFF9B93FF);

  // ==================== 浅色主题颜色 ====================
  
  /// 浅色背景色
  static const Color lightBackground = Color(0xFFF8F9FE);
  
  /// 浅色卡片色
  static const Color lightCard = Colors.white;
  
  /// 浅色分割线
  static const Color lightDivider = Color(0xFFE0E0E0);

  /// 浅色主要文字
  static const Color lightTextPrimary = Color(0xFF2D3436);
  
  /// 浅色次要文字
  static const Color lightTextSecondary = Color(0xFF636E72);

  // ==================== 深色主题颜色 ====================
  
  /// 深色背景色
  static const Color darkBackground = Color(0xFF121212);
  
  /// 深色卡片色
  static const Color darkCard = Color(0xFF1E1E1E);
  
  /// 深色分割线
  static const Color darkDivider = Color(0xFF2D2D2D);

  /// 深色主要文字
  static const Color darkTextPrimary = Color(0xFFFAFAFA);
  
  /// 深色次要文字
  static const Color darkTextSecondary = Color(0xFFB0B0B0);

  // ==================== 心情颜色 ====================
  
  /// 开心 - 绿色
  static const Color moodHappy = Color(0xFF27AE60);
  
  /// 平静 - 蓝色
  static const Color moodCalm = Color(0xFF3498DB);
  
  /// 一般 - 橙色
  static const Color moodNeutral = Color(0xFFF39C12);
  
  /// 难过 - 红色
  static const Color moodSad = Color(0xFFE74C3C);
  
  /// 疲惫 - 紫色
  static const Color moodTired = Color(0xFF9B59B6);

  // ==================== 功能色 ====================
  
  /// 成功色
  static const Color success = Color(0xFF27AE60);
  
  /// 警告色
  static const Color warning = Color(0xFFF39C12);
  
  /// 错误色
  static const Color error = Color(0xFFE74C3C);
  
  /// 信息色
  static const Color info = Color(0xFF3498DB);
}

3.2 主题数据配置

// lib/mental_health/theme/app_theme.dart

import 'package:flutter/material.dart';
import 'app_colors.dart';

/// App 主题配置
class AppTheme {
  /// 浅色主题
  static ThemeData get lightTheme => ThemeData(
    useMaterial3: true,
    brightness: Brightness.light,
    
    // 主色调
    primaryColor: AppColors.primary,
    colorScheme: ColorScheme.light(
      primary: AppColors.primary,
      primaryContainer: AppColors.primaryLight,
      secondary: AppColors.primary,
      surface: AppColors.lightCard,
      error: AppColors.error,
    ),
    
    // Scaffold 背景
    scaffoldBackgroundColor: AppColors.lightBackground,
    
    // AppBar 主题
    appBarTheme: const AppBarTheme(
      backgroundColor: AppColors.lightCard,
      foregroundColor: AppColors.lightTextPrimary,
      elevation: 0,
      centerTitle: true,
      titleTextStyle: TextStyle(
        color: AppColors.lightTextPrimary,
        fontSize: 18,
        fontWeight: FontWeight.w600,
      ),
    ),
    
    // 卡片主题
    cardTheme: CardTheme(
      color: AppColors.lightCard,
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
    ),
    
    // 按钮主题
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        backgroundColor: AppColors.primary,
        foregroundColor: Colors.white,
        elevation: 0,
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
    
    // 文字按钮主题
    textButtonTheme: TextButtonThemeData(
      style: TextButton.styleFrom(
        foregroundColor: AppColors.primary,
      ),
    ),
    
    // 输入框主题
    inputDecorationTheme: InputDecorationTheme(
      filled: true,
      fillColor: AppColors.lightCard,
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: BorderSide.none,
      ),
      enabledBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: const BorderSide(color: AppColors.lightDivider),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: const BorderSide(color: AppColors.primary, width: 2),
      ),
      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
    ),
    
    // 底部导航栏主题
    bottomNavigationBarTheme: const BottomNavigationBarThemeData(
      backgroundColor: AppColors.lightCard,
      selectedItemColor: AppColors.primary,
      unselectedItemColor: AppColors.lightTextSecondary,
      type: BottomNavigationBarType.fixed,
      elevation: 8,
    ),
    
    // 分割线主题
    dividerTheme: const DividerThemeData(
      color: AppColors.lightDivider,
      thickness: 1,
    ),
    
    // 图标主题
    iconTheme: const IconThemeData(
      color: AppColors.lightTextSecondary,
    ),
  );

  /// 深色主题
  static ThemeData get darkTheme => ThemeData(
    useMaterial3: true,
    brightness: Brightness.dark,
    
    // 主色调
    primaryColor: AppColors.primary,
    colorScheme: ColorScheme.dark(
      primary: AppColors.primary,
      primaryContainer: AppColors.primaryDark,
      secondary: AppColors.primary,
      surface: AppColors.darkCard,
      error: AppColors.error,
    ),
    
    // Scaffold 背景
    scaffoldBackgroundColor: AppColors.darkBackground,
    
    // AppBar 主题
    appBarTheme: const AppBarTheme(
      backgroundColor: AppColors.darkCard,
      foregroundColor: AppColors.darkTextPrimary,
      elevation: 0,
      centerTitle: true,
      titleTextStyle: TextStyle(
        color: AppColors.darkTextPrimary,
        fontSize: 18,
        fontWeight: FontWeight.w600,
      ),
    ),
    
    // 卡片主题
    cardTheme: CardTheme(
      color: AppColors.darkCard,
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16),
      ),
    ),
    
    // 按钮主题
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        backgroundColor: AppColors.primary,
        foregroundColor: Colors.white,
        elevation: 0,
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
    
    // 文字按钮主题
    textButtonTheme: TextButtonThemeData(
      style: TextButton.styleFrom(
        foregroundColor: AppColors.primary,
      ),
    ),
    
    // 输入框主题
    inputDecorationTheme: InputDecorationTheme(
      filled: true,
      fillColor: AppColors.darkCard,
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: BorderSide.none,
      ),
      enabledBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: const BorderSide(color: AppColors.darkDivider),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
        borderSide: const BorderSide(color: AppColors.primary, width: 2),
      ),
      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
    ),
    
    // 底部导航栏主题
    bottomNavigationBarTheme: const BottomNavigationBarThemeData(
      backgroundColor: AppColors.darkCard,
      selectedItemColor: AppColors.primary,
      unselectedItemColor: AppColors.darkTextSecondary,
      type: BottomNavigationBarType.fixed,
      elevation: 8,
    ),
    
    // 分割线主题
    dividerTheme: const DividerThemeData(
      color: AppColors.darkDivider,
      thickness: 1,
    ),
    
    // 图标主题
    iconTheme: const IconThemeData(
      color: AppColors.darkTextSecondary,
    ),
  );
}

四、主题 Provider

// lib/mental_health/providers/theme_provider.dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

/// 主题模式
enum AppThemeMode {
  system('跟随系统', Icons.brightness_auto),
  light('浅色模式', Icons.light_mode),
  dark('深色模式', Icons.dark_mode);

  final String label;
  final IconData icon;

  const AppThemeMode(this.label, this.icon);
}

/// 主题 Provider
class ThemeProvider extends ChangeNotifier {
  static const String _themeModeKey = 'theme_mode';
  
  ThemeMode _themeMode = ThemeMode.system;
  bool _isLoading = true;

  ThemeMode get themeMode => _themeMode;
  bool get isLoading => _isLoading;
  
  /// 获取当前的 AppThemeMode
  AppThemeMode get appThemeMode {
    switch (_themeMode) {
      case ThemeMode.system:
        return AppThemeMode.system;
      case ThemeMode.light:
        return AppThemeMode.light;
      case ThemeMode.dark:
        return AppThemeMode.dark;
    }
  }

  /// 初始化
  Future<void> initialize() async {
    _isLoading = true;
    notifyListeners();

    try {
      final prefs = await SharedPreferences.getInstance();
      final savedMode = prefs.getString(_themeModeKey);
      
      if (savedMode != null) {
        _themeMode = _parseThemeMode(savedMode);
      }
    } catch (e) {
      debugPrint('加载主题设置失败: $e');
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }

  /// 设置主题模式
  Future<void> setThemeMode(AppThemeMode mode) async {
    _themeMode = _parseAppThemeMode(mode);
    
    // 保存设置
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(_themeModeKey, mode.name);
    
    notifyListeners();
  }

  /// 切换主题
  Future<void> toggleTheme() async {
    final currentMode = appThemeMode;
    final nextMode = (currentMode.index + 1) % AppThemeMode.values.length;
    await setThemeMode(AppThemeMode.values[nextMode]);
  }

  ThemeMode _parseThemeMode(String mode) {
    switch (mode) {
      case 'system':
        return ThemeMode.system;
      case 'light':
        return ThemeMode.light;
      case 'dark':
        return ThemeMode.dark;
      default:
        return ThemeMode.system;
    }
  }

  ThemeMode _parseAppThemeMode(AppThemeMode mode) {
    switch (mode) {
      case AppThemeMode.system:
        return ThemeMode.system;
      case AppThemeMode.light:
        return ThemeMode.light;
      case AppThemeMode.dark:
        return ThemeMode.dark;
    }
  }
}

五、主题设置页面

// lib/mental_health/screens/theme_settings_screen.dart

import 'package:flutter/material.dart';
import '../providers/theme_provider.dart';

class ThemeSettingsScreen extends StatelessWidget {
  const ThemeSettingsScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('主题设置'),
      ),
      body: Consumer<ThemeProvider>(
        builder: (context, provider, child) {
          if (provider.isLoading) {
            return const Center(child: CircularProgressIndicator());
          }

          return ListView(
            padding: const EdgeInsets.all(16),
            children: [
              // 当前主题预览
              _buildThemePreview(context),
              
              const SizedBox(height: 24),
              
              // 主题选项
              const Text(
                '选择主题模式',
                style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                ),
              ),
              const SizedBox(height: 12),
              
              // 主题选项列表
              ...AppThemeMode.values.map((mode) {
                final isSelected = provider.appThemeMode == mode;
                return _buildThemeOption(
                  context,
                  provider,
                  mode,
                  isSelected,
                );
              }),
            ],
          );
        },
      ),
    );
  }

  Widget _buildThemePreview(BuildContext context) {
    final theme = Theme.of(context);
    
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '主题预览',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            
            Row(
              children: [
                // 浅色预览
                Expanded(
                  child: Container(
                    padding: const EdgeInsets.all(12),
                    decoration: BoxDecoration(
                      color: const Color(0xFFF8F9FE),
                      borderRadius: BorderRadius.circular(12),
                      border: Border.all(color: Colors.grey[300]!),
                    ),
                    child: Column(
                      children: [
                        Container(
                          width: 40,
                          height: 40,
                          decoration: BoxDecoration(
                            color: theme.primaryColor,
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                        const SizedBox(height: 8),
                        Text(
                          '浅色',
                          style: TextStyle(
                            fontSize: 12,
                            color: Colors.grey[700],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
                
                const SizedBox(width: 12),
                
                // 深色预览
                Expanded(
                  child: Container(
                    padding: const EdgeInsets.all(12),
                    decoration: BoxDecoration(
                      color: const Color(0xFF121212),
                      borderRadius: BorderRadius.circular(12),
                      border: Border.all(color: Colors.grey[700]!),
                    ),
                    child: Column(
                      children: [
                        Container(
                          width: 40,
                          height: 40,
                          decoration: BoxDecoration(
                            color: theme.primaryColor,
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                        const SizedBox(height: 8),
                        Text(
                          '深色',
                          style: TextStyle(
                            fontSize: 12,
                            color: Colors.grey[300],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildThemeOption(
    BuildContext context,
    ThemeProvider provider,
    AppThemeMode mode,
    bool isSelected,
  ) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: RadioListTile<AppThemeMode>(
        value: mode,
        groupValue: provider.appThemeMode,
        onChanged: (value) {
          if (value != null) {
            provider.setThemeMode(value);
          }
        },
        title: Row(
          children: [
            Icon(mode.icon),
            const SizedBox(width: 12),
            Text(mode.label),
          ],
        ),
        subtitle: Text(_getDescription(mode)),
        secondary: isSelected
            ? Container(
                padding: const EdgeInsets.all(4),
                decoration: BoxDecoration(
                  color: Theme.of(context).primaryColor,
                  shape: BoxShape.circle,
                ),
                child: const Icon(
                  Icons.check,
                  color: Colors.white,
                  size: 16,
                ),
              )
            : null,
      ),
    );
  }

  String _getDescription(AppThemeMode mode) {
    switch (mode) {
      case AppThemeMode.system:
        return '根据系统设置自动切换';
      case AppThemeMode.light:
        return '始终使用浅色模式';
      case AppThemeMode.dark:
        return '始终使用深色模式';
    }
  }
}

六、在应用中使用主题

// main.dart

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化主题 Provider
  final themeProvider = ThemeProvider();
  await themeProvider.initialize();
  
  runApp(
    ChangeNotifierProvider.value(
      value: themeProvider,
      child: const MentalHealthApp(),
    ),
  );
}

class MentalHealthApp extends StatelessWidget {
  const MentalHealthApp({super.key});

  
  Widget build(BuildContext context) {
    return Consumer<ThemeProvider>(
      builder: (context, themeProvider, child) {
        return MaterialApp(
          title: '心理健康',
          debugShowCheckedModeBanner: false,
          
          // 主题模式
          themeMode: themeProvider.themeMode,
          
          // 浅色主题
          theme: AppTheme.lightTheme,
          
          // 深色主题
          darkTheme: AppTheme.darkTheme,
          
          home: const MentalHealthHomeScreen(),
        );
      },
    );
  }
}

七、自定义组件的主题适配

7.1 使用 Theme.of(context)

// 自定义组件使用主题颜色
class MoodCard extends StatelessWidget {
  final Color moodColor;
  final String label;

  const MoodCard({
    super.key,
    required this.moodColor,
    required this.label,
  });

  
  Widget build(BuildContext context) {
    // 使用 context 获取主题
    final theme = Theme.of(context);
    
    return Card(
      color: theme.cardColor,  // 自动适配主题
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            Container(
              width: 60,
              height: 60,
              decoration: BoxDecoration(
                color: moodColor.withOpacity(0.2),
                shape: BoxShape.circle,
              ),
              child: Icon(
                Icons.mood,
                color: moodColor,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              label,
              style: TextStyle(
                color: theme.textTheme.bodyLarge?.color,  // 自动适配文字颜色
              ),
            ),
          ],
        ),
      ),
    );
  }
}

7.2 使用 ColorScheme

// 使用 ColorScheme
class ThemedButton extends StatelessWidget {
  const ThemedButton({super.key});

  
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    
    return ElevatedButton(
      onPressed: () {},
      style: ElevatedButton.styleFrom(
        backgroundColor: colorScheme.primary,  // 使用主色
        foregroundColor: colorScheme.onPrimary,   // 使用主色上的文字色
      ),
      child: const Text('按钮'),
    );
  }
}

八、鸿蒙平台专属适配

适配点:鸿蒙系统主题检测

问题:鸿蒙设备的主题检测可能和标准 Android 不同。

解决方案

// 在 ThemeProvider 中添加鸿蒙特定逻辑
Future<void> _detectSystemTheme() async {
  // 鸿蒙设备使用 MediaQuery 检测
  final brightness = MediaQuery.platformBrightnessOf(context);
  
  switch (brightness) {
    case Brightness.light:
      _themeMode = ThemeMode.light;
      break;
    case Brightness.dark:
      _themeMode = ThemeMode.dark;
      break;
  }
}

九、我的踩坑记录

坑1:主题切换闪烁

问题:切换主题时页面闪烁。

原因:没有使用 themeMode 参数。

错误代码

// ❌ 错误
MaterialApp(
  theme: isDark ? darkTheme : lightTheme,
  home: HomePage(),
)

解决代码

// ✅ 正确
MaterialApp(
  theme: lightTheme,
  darkTheme: darkTheme,
  themeMode: currentThemeMode,  // 使用 themeMode
  home: HomePage(),
)

坑2:自定义颜色不响应主题

问题:手动设置的颜色不随主题切换。

原因:使用了硬编码的颜色。

解决代码

// ✅ 使用主题颜色
Container(
  color: Theme.of(context).colorScheme.surface,
)

坑3:深色模式文字不清晰

问题:深色模式下文字看不清楚。

原因:使用了浅色主题的文字颜色。

解决代码

// ✅ 使用主题的文字颜色
Text(
  '文字',
  style: TextStyle(
    color: Theme.of(context).textTheme.bodyLarge?.color,
  ),
)

十、功能验证清单

序号 检查项 测试场景 预期结果
1 浅色主题 选择浅色模式 背景变白
2 深色主题 选择深色模式 背景变黑
3 系统主题 系统切换主题 App 跟随变化
4 主题保存 退出 App 主题设置保留
5 组件适配 各页面 所有组件正确响应主题

十一、大一学生真实学习总结

深色模式让我彻底理解了 Flutter 的主题系统。

最重要的几点:

  1. 不要硬编码颜色

    • 所有颜色都要使用主题颜色
    • 这样才能响应主题切换
  2. themeMode 是关键

    • 使用 themeMode 而不是直接切换 theme
    • 这样动画更平滑,不会闪烁
  3. ColorScheme 是好帮手

    • 合理使用 ColorScheme
    • 减少自定义颜色
  4. 测试要全面

    • 浅色模式测试一遍
    • 深色模式测试一遍
    • 系统模式测试一遍

作者:IntMainJhy
创作时间:2026年5月
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐