2024 Flutter 适配暗黑模式,附 GIF 效果图

使用 Flutter 给 App 适配暗黑模式相对来说比较容易实现,因为 Flutter 本身已经提供了很多接口,直接使用就行了,简单搭建个 Demo 玩玩是可以的。但如果真做项目,需要多考虑一步,那就是文本样式的适配。如何在暗黑模式和浅色模式下,对文本颜色做适配,怎么做比较规范,看起来不那么乱

我看了一些开源项目,也自己琢磨了一套目前使用的方案,用着还行,分享在这。

归纳文本样式

将整个项目设计图的文本做个浏览,一般来说,设计图使用的文本都是有一定规范,大小和颜色就那么几套,不会天马行空没点规律,随处取色。所以,首先找出使用到文本的大小有哪些,其次,将使用到的颜色,能分类的颜色提取出来,分成浅色模式和暗黑模式。现在很多设计软件,像 Figma 都有提供一键渲染暗黑模式设计图,这个步骤不难。

class ColorStyle {
  // 浅色模式
  static const Color primary = Colors.blue;
  static const Color accent = Colors.amber;
  static const Color background = Colors.white;
  static const Color foreground = Colors.black;
  static const Color textPrimary = Colors.black87;
  static const Color textSecondary = Colors.black54;

  // 暗黑模式
  static Color darkPrimary = Colors.blue[700]!;
  static Color darkAccent = Colors.amber[200]!;
  static const Color darkBackground = Colors.black;
  static const Color darkForeground = Colors.white;
  static const Color darkTextPrimary = Colors.white70;
  static const Color darkTextSecondary = Colors.white60;

  // 共用颜色
  static const Color success = Colors.green;
  static const Color warning = Colors.orange;
  static const Color error = Colors.red;
  static const Color info = Colors.lightBlue;
}

接着定义文本样式,按照字体大小定义不同颜色的文本样式,可能存在同一个字体大小有不同的颜色。比如设计稿里 12 号字体,可能会有两种,甚至三种颜色。对这些文本,我们不能使用 TextTheme 规范定义的,而是要单独定义暗黑模式下的样式

class AppTextTheme {

  static final TextStyle title17 = TextStyle(fontSize: 17.0, fontWeight: FontWeight.w500 ,color: ColorStyle.foreground);
  static final TextStyle title16 = TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500, color: ColorStyle.background);
  static final TextStyle body15 = TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500, color: ColorStyle.foreground);
  static final TextStyle body14Black = TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: ColorStyle.foreground);
  static final TextStyle label13Black = TextStyle(fontSize: 13.0, color: ColorStyle.textSecondary);
  static final TextStyle label13Blue = TextStyle(fontSize: 13.0,  color: ColorStyle.primary);
  static final TextStyle label12Black = TextStyle(fontSize: 12.0, color: ColorStyle.textSecondary);
  static final TextStyle label12Blue = TextStyle(fontSize: 12.0, color: ColorStyle.primary);
  static final TextStyle label11 = TextStyle(fontSize: 11.0, color: ColorStyle.textSecondary);
  /// 自定义暗黑模式下的蓝色文本样式
  static final TextStyle label12BlueDark = TextStyle(fontSize: 12.0, color: ColorStyle.darkPrimary);
  static final TextStyle label13BlueDark = TextStyle(fontSize: 13.0, color: ColorStyle.darkPrimary);
  static final TextStyle body14WhiteDark = TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: ColorStyle.background);

  static TextTheme lightTextTheme = TextTheme(
    titleMedium: title17,
    titleSmall: title16,
    bodyMedium: body15,
    bodySmall: body14Black,
    labelLarge: label13Black,
    labelMedium: label12Black,
    labelSmall: label11,
  );

  static TextTheme darkTextTheme = TextTheme(
    titleMedium: title17.copyWith(color: ColorStyle.darkForeground),
    titleSmall: title16.copyWith(color: ColorStyle.darkBackground),
    bodyMedium: body15.copyWith(color: ColorStyle.darkForeground),
    bodySmall: body14Black.copyWith(color: ColorStyle.darkForeground),
    labelLarge: label13Black.copyWith(color: ColorStyle.darkTextSecondary),
    labelMedium: label12Black.copyWith(color: ColorStyle.darkTextSecondary),
    labelSmall: label11.copyWith(color: ColorStyle.darkTextSecondary),
  );

}

然后就是配置由 Materia 提供的暗黑模式接口

  ///主题颜色
  theme: AppTheme.lightThemeData,
  darkTheme: AppTheme.darkThemeData,

现在给 UI 布局使用上面的文本,比如看这个布局

image-20231122101811182

大部分文本都可以使用已经定义规范好的 TextTheme。比如 titleMediumlabelMedium

Text('欢迎使用', style: Theme
    .of(context)
    .textTheme
    .titleMedium),
Text('Sign in to continue', style: Theme
    .of(context)
    .textTheme
    .labelMedium),
TextField(
  decoration: InputDecoration(
    labelText: '你的账号',
    labelStyle: Theme.of(context).textTheme.labelMedium,
    prefixIcon: Icon(Icons.person),
  ),
),    

如果碰到不能兼容使用的,则要单独设置,分别设置好浅色模式和暗黑模式使用的文本样式。

child: Text('登录', style: Theme.of(context).brightness == Brightness.dark ? AppTextTheme.body14WhiteDark : AppTextTheme.body14Black,),

适配暗黑模式

我们大概想实现一个什么样的效果呢?下面是微信的深色模式,App 的主题默认跟随系统,也就是说系统设置是暗黑模式,则 App 展示暗黑模式,如果是浅色模式,App 则浅色。也可以自己定义,就是要单独给 App 定义一个不同于系统的主题模式,这就是手动选择。

image-20231122102607715

那在 Flutter MaterialApp 决定主题样式的对象是 ThemeMode

enum ThemeMode {
  system,
  light,
  dark,
}

看命名大概就清楚这三个属性的含义,分别是跟随系统、浅色模式、暗黑模式。这三个属性在这里设置:

///主题颜色
theme: AppTheme.lightThemeData,
darkTheme: AppTheme.darkThemeData,
themeMode: ThemeMode.system,

默认不设置就是跟随系统 ThemeMode.system,如果你将它修改成 ThemeMode.dark ,那就算系统设置成浅色模式,这 App 还是固定成暗黑模式。接下来看这个布局

image-20231122103903903

我使用 GetX 管理状态,这里就直接贴代码里。

class DarkModePage extends GetView<ThemeController> {

  const DarkModePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("深色模式"),
      ),
      body: Obx((){
        return Column(
          children: [
            SwitchListTile(
              title: Text('跟随系统'),
              value: controller.isSystemTheme.value,
              onChanged: controller.toggleSystemTheme,
            ),
            Visibility(
              visible: !controller.isSystemTheme.value,
              child: Column(
                children: [
                  RadioListTile<bool>(
                    title: const Text('浅色模式'),
                    value: false,
                    groupValue: controller.isDarkMode.value,
                    onChanged: controller.toggleDarkMode,
                  ),
                  RadioListTile<bool>(
                    title: const Text('深色模式'),
                    value: true,
                    groupValue: controller.isDarkMode.value,
                    onChanged: controller.toggleDarkMode,
                  ),
                ],
              ),
            )
          ],
        );
      }),
    );
  }
}

然后是 Controller ,这里大家情况都不一样,可以做个参考,关键代码我加了注释,还有这个 Controller 一定要最早初始化,添加到 GetX 中管理。

class ThemeController extends BaseGetController {
  var isSystemTheme = true.obs; // 跟随系统主题
  var isDarkMode = false.obs; // 深色模式

  @override
  void onInit() {
    super.onInit();
  }

  ThemeMode get currentThemeMode {
    if (isSystemTheme.value) {
      return ThemeMode.system;
    } else {
      return isDarkMode.value ? ThemeMode.dark : ThemeMode.light;
    }
  }

  void toggleSystemTheme(bool value) {
    isSystemTheme.value = value;
    if (!value) {
      // 如果不跟随系统,则默认设置为浅色模式
      isDarkMode.value = false;
    }
    updateThemeMode();
  }

  void toggleDarkMode(bool? value) {
    if (!isSystemTheme.value) {
      isDarkMode.value = value!;
      updateThemeMode();
    }
  }

  void updateThemeMode() {
    Get.changeThemeMode(currentThemeMode); // 通知框架切换主题
  }

  @override
  void onClose() {
    super.onClose();
  }

}

因为 MaterialApp 要使用ThemeController 里的变量,所以要先添加。

class Injection{

  static Future<void> init() async{
    await Get.putAsync(() => SharedPreferences.getInstance());
    Get.lazyPut(() => RequestRepository());
    Get.put(BaseGetController());
    Get.put(ThemeController());
  }

}


ThemeController themeController = Get.find<ThemeController>();
themeMode: themeController.currentThemeMode,

接下来就是对主题做缓存,将用户的主题偏好保存起来,这样下次打开 App 时可以读取之前设置的主题,不然用户切换了暗黑模式,结果下次打开还是浅色模式,这肯定不对,一般使用 shared_preferences 保存就够了。

初始化加载缓存

import 'package:shared_preferences/shared_preferences.dart';

class ThemeController extends GetxController {
  var isSystemTheme = true.obs; 
  var isDarkMode = false.obs; 

  @override
  void onInit() {
    super.onInit();
    loadThemeMode();
  }

  Future<void> saveThemeMode() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool('isSystemTheme', isSystemTheme.value);
    await prefs.setBool('isDarkMode', isDarkMode.value);
  }

  Future<void> loadThemeMode() async {
    final prefs = await SharedPreferences.getInstance();
    isSystemTheme.value = prefs.getBool('isSystemTheme') ?? true;
    isDarkMode.value = prefs.getBool('isDarkMode') ?? false;
  }
}

更改主题时缓存

void toggleSystemTheme(bool value) {
  isSystemTheme.value = value;
  if (!value) {
    isDarkMode.value = false;
  }
  saveThemeMode();
  updateThemeMode();
}

void toggleDarkMode(bool? value) {
  if (!isSystemTheme.value && value != null) {
    isDarkMode.value = value;
    saveThemeMode();
    updateThemeMode();
  }
}

感觉有点虎头蛇尾,但每个人情况不一样,加上这里面涉及状态管理部分内容,我这直接使用 GetX,但很多人可能使用 Provider,大家情况都不一样,我只能尽量多提供逻辑清晰的代码,关键代码加了注释,大概就这样。

本文由老郭种树原创,转载请注明:https://guozh.net/2024-flutter-adapts-to-dark-mode/

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注