> 为什么需要深色模式?

用户使用深色模式的理由有很多,总结其中比较突出的是下面三点:

Google 在 Android P 当中正式推出了自己的深色模式,Apple 也在 iOS 13 正式推出系统级的深色模式(在此之前,macOS 10.14 Mojave 上已经加入了 Dark Mode)。

在 Android 和 iOS 都正式推出系统级的深色模式后,当用户开启系统全局的深色模式后突然出现一个不支持的 App,这个 App 就会显得特别刺眼,甚至有些用户可能会不得不去寻找支持这一模式的替代品。所以,在项目资源(设计师、工程师、工时)允许的情况下,还是建议为 App 适配好 Dark Mode 的。 与之类似的就是 App 的多语言支持,当系统设置某种语言时,应用内的文字也相应变化。

下面主要从技术角度阐述如何在 Flutter App 上适配 Dark Mode,给用户更好的系统一致性体验。

> 在 Flutter 上的基本实现

我们后面需要使用 provider 包来作为状态管理工具,先把它添加到 pubspec.yaml 文件中,目前最新版本的添加方式如下:

dependencies:
  provider: ^4.3.1

Flutter 在 MaterialApp 中已经提供了themedarkTheme 两个参数让我们设置两种模式下的颜色及文字样式。接收的 ThemeData 值,几乎涵盖了所有 Material Widget 中所使用的颜色及主题,我们只需要在 ThemeData 值中适配好 App 基于不同 theme 需要用到的不同样式即可。而如果使用的 CupertinoApp ,目前最新稳定版 Flutter(v1.17.5) 只提供了 theme 参数,而 darkTheme 尚不支持。GitHub 上也有人提了相关 issue,将来应该也会支持的。

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider.value(value: DisplayModel()),
        ChangeNotifierProvider.value(value: LocaleModel())
      ],
      child: Consumer2<DisplayModel, LocaleModel>(
        builder: (context, displayModel, localeModel, _) {
          return MaterialApp(
           ***
            theme: displayModel.themeDate(),
            darkTheme: displayModel.themeDate(darkTheme: true),
            // themeMode 可以用于设置 App 主题是依赖于系统设定,还是手动设定
            themeMode: displayModel.themeMode,
            ***
          );
        },
      ),
    );
  }

主题配置好之后,我们需要建一个主题变更通知类,来实现主题的动态切换。

class DisplayModel extends ChangeNotifier {
  // 主题颜色
  MaterialColor _materialColor = themeColorMap[SpHelper.getThemeColor()];

  // 外观模式
  ThemeMode _themeMode = SpHelper.getThemeMode();

  // 字体选择
  String _fontName = SpHelper.getFontFamily();

  get materialColor => _materialColor;

  get themeMode => _themeMode;

  get fontName => _fontName;

  ThemeData themeDate({bool darkTheme = false}) => ThemeData(
        primarySwatch: _materialColor,
        fontFamily: _fontName,
        brightness: darkTheme ? Brightness.dark : Brightness.light,
      );

  switchColor(String newColor) {
    _materialColor = themeColorMap[newColor];
    notifyListeners();
    SpHelper.sp.setString(SP_THEME_COLOR, newColor);
  }

  switchThemeMode(ThemeMode newThemeMode) {
    _themeMode = newThemeMode;
    notifyListeners();
    SpHelper.sp.setString(
        SP_THEME_MODE,
        (_themeMode == ThemeMode.system)
            ? THEME_MODE_SYSTEM
            : (_themeMode == ThemeMode.light
                ? THEME_MODE_LIGHT
                : THEME_MODE_DARK));
  }

  switchFont(String newFontName) {
    _fontName = newFontName;
    notifyListeners();
    SpHelper.sp.setString(SP_FONT_FAMILY, newFontName);
  }
}

这样就配置好了主题获取和变更通知,接下来只需要在 App 设置页面,操作模式切换的地方(通常可以是 RadioListTileSwitchListTile 等)调用:

onChanged: (newValue) {
                setState(() {
                  _currentAppearance = newValue;
                  // 触发主题变更
                  Provider.of<DisplayModel>(context, listen: false)
                      .switchThemeMode(newValue);
                });
},

需要额外注意的一点是,这里实现的跟随系统功能是需要判断设备操作系统版本的。因为如开头所诉,系统级的深色模式,是从 Android P 和 iOS 13 才开始支持的。如果设备系统版本低于它们,则只能实现在深色和浅色两种模式中手动来切换。

放几张适配后的效果图给大家看看:

详细的代码以及实现细节,可以参看 V2LF 项目的代码。

> 总结

App 技术层面的 Dark Mode 的适配,实现起来其实并不复杂。个人感觉,如果想要把 App 深色模式的体验做好,更多的还是考验设计师的设计能力。甚至还需要研究不同操作系统本身对深色模式给出的视觉规范。