在 flutter 中如何使用和扩展 `ThemeData`

前言

做过UI开发的同学都知道,在开发中我们通常会将 文字大小色值 等内容放在配置文件中,通过统一的管理类来读取(严禁在UI代码中写死)。以便后续调整时不用修改源码,只需要修改配置文件即可。

例如这样:

  1. 定义常量存放
/// 存放颜色常量
abstract class ColorConfigs {
  static const Color background = Color(0xFFFF6600);
  static const Color textHint = Color(0xFFA0A4A7);
}
  1. 通过统一获取
/// GOOD
Container(
  //通过 ColorConfig 获取色值
  color: ColorConfigs.background,
)

/// BAD
Container(
  //写死
  color: Color(0xFFFF6600),
)

Flutter为我们提供了Theme类,可以让我们节省封装常量配置类(如上示例中的 ColorConfigs)的步骤。将色值、字体风格等配置内容存入ThemeData中,子控件可统一通过 Theme.of(context)读取 color、textStyle、等配置信息。

本篇通过换肤demo,介绍在flutter项目中如何使用 theme 以及如何对 themeData 进行字段扩展,实现全局的主题配置管理。

Theme 的基本使用方式

1. Theme 的注册

MaterialApp(
  theme: myThemeData, //一个ThemeData的实例,下面提供具体代码
  home: BodyWidget(),
)

我们做全局的主体配置,在 MaterialApp 中对 theme 字段进行入参赋值。示例代码中的 myThemeData 是一个 ThemeData 的实现实例,可通过 ThemeData 的构造方法来查看其可供保存的主体及样式信息,按照各自所需进行参数赋值。

下面是小编在自己项目中用到的ThemeData配置项,定义了各种状态颜色字体样式、可供参考:

myThemeData

val myThemeData = ThemeData(
  primaryColor: Colors.white,
  disabledColor: const Color(0xffcbced0),
  backgroundColor: const Color(0xfff3f4f5),
  hintColor: const Color(0xffe2e5e7),
  errorColor: const Color(0xffe21a1a),
  highlightColor: const Color(0xffa7d500),
  shadowColor: const Color(0xffa0a4a7),
  selectedRowColor: const Color(0xfff3f4f5),
  colorScheme: const ColorScheme.light(
    primary: Colors.white,
    secondary: Color(0xffa7d500),
    background: Color(0xfff3f4f5),
    error: Color(0xffe21a1a),
    onPrimary: Color(0xff242524),
    onError: Colors.white,
    onBackground: Color(0xffe2e5e7),
    onSecondary: Color(0xff707275),
  ),
  textTheme: TextTheme(
    headline1: TextStyle(
      fontSize: 17.sp,
      fontWeight: FontWeight.bold,
      color: const Color(0xff242524),
    ),
    headline2: TextStyle(
      fontSize: 16.sp,
      fontWeight: FontWeight.bold,
      color: const Color(0xff242524),
    ),
    ...中间省略 healin3 ~ headline5,只是配置不一样
    headline6: TextStyle(
      fontSize: 14.sp,
      fontWeight: FontWeight.bold,
      color: const Color(0xff707275),
    ),
    subtitle1: TextStyle(
      fontSize: 12.sp,
      fontWeight: FontWeight.w500,
      color: const Color(0xff242524),
    ),
    subtitle2: TextStyle(
      fontSize: 12.sp,
      fontWeight: FontWeight.w500,
      color: const Color(0xff707275),
    ),
    bodyText1: TextStyle(
      fontSize: 11.sp,
      fontWeight: FontWeight.normal,
      color: const Color(0xff242524),
    ),
    bodyText2: TextStyle(
      fontSize: 11.sp,
      fontWeight: FontWeight.normal,
      color: const Color(0xff242524),
    ),
  ),
)

2. 读取 ThemeData 里的配置:

@override
Widget build(BuildContext context) {
  return Container(
    color: Theme.of(context).backgroundColor,
    child: Text(
        'hellow', 
        style: Theme.of(context).headline).bodyText1,
  );
}
  • Theme.of(context).backgroundColor:读取主题配置中的背景颜色,在 myThemeData 中进行过赋值操作
  • Theme.of(context).headline).bodyText1:读取主题配置中键值为 bodyText1 的字体样式
小技巧介绍

通常为了便于开发阅读,我们也可以使用extension对 ThemeData 内属性进行重命名获取:

新建 extension_theme.dart,文件名字随意:

///用于重命名颜色属性
extension ThemeDataColorExtension on ThemeData {
  Color get bgColor => colorScheme.onBackground;
 ...
}
///用于重命名字体样式属性
extension ThemeDataTextStyleExtension on ThemeData {
  TextStyle get bodyStyle => textTheme.bodyText1!;
 ...
}

在UI页面进行引用导入使用,上面的 demo 可改为:

import ./extension_theme.dart
...

@override
Widget build(BuildContext context) {
  return Container(
    color: Theme.of(context).bgColor,
    child: Text(
        'hellow', 
        style: Theme.of(context).bodyStyle,
  );
}

ThemeData 内置字段不够用,如何扩展?

ThemeData 的构造函数中我们可以看到,ThemeData 内置的字段是有限的。假如我们的UI设计包含的色值数量或者字体样式数量超出了 ThemeData 可供设置数量怎么办呢?

比如:我们想新增一个色值配置,名字就叫 connerColor,我们还想保持统一,一律通过 ThemeData 来统一读取统一配置,要如何处理呢?

小编在项目里是这么做的,将 ThemeData 进行一层封装,以新增 connerColor 为例,具体代码请看👇🏻一键换肤代码介绍。

如何实现一键换肤

有了ThemeData作为统一管理存放配置信息后,实现一键换肤的思路就很清晰了,大致是这样的:

从上图可以看到,除了需要ThemeData用于存放配置信息,我们还需要封装一个监听类用于监听选中主题发生变更,这个功能我们在下面用provider来实现。

1. 首先在 yaml 新增引入 provider

dependencies:
  provider: ^6.0.2

2. 创建主题枚举,假设我们提供两种主题切换

///主题类型
enum ThemeEnum {
  yellow,
  red,
}

3. 我们对 ThemeData 进行一层封装处理,添加 connerColor 进行颜色字段扩展

///自定义模型,包装一下 themeData
class ThemeItem {
  final ThemeEnum themeEnum;
  final ThemeData themeData;

  // 扩展一个字段,用于表示自定义色值
  final Color connerColor;

  ThemeItem(
    this.themeEnum,
    this.themeData, {
    required this.connerColor,
  });
}

4. 创建一个主题管理类 ThemeConfig

abstract class ThemeConfig {
  ///记录当前选中主题
  static late ThemeItem _currentTheme;

  static ThemeData get currentThemeData => _currentTheme.themeData;
  static ThemeEnum get currentTheme => _currentTheme.themeEnum;

  ///提供获取扩展的色值
  static Color? get connerColor => _currentTheme.connerColor;

  ///设置选中主题,提供外部调用,更换当前主题
  static void initTheme(ThemeItem theme) {
    _currentTheme = theme;
  }
}

5. 为保持统一通过ThemeData进行读取,使用extension对新增字段connerColor进行读取扩展

extension ExTheme on ThemeData {

  ///扩展获取自定义色值
  Color get connerColor => ThemeConfig.connerColor!;
}

6. 基于 provider 的使用,我们添加一个工具类,用于通知设置主题变更

class AppInfoProvider with ChangeNotifier {
  ThemeData get currentTheme => ThemeConfig.currentThemeData;

  ///切换主题
  setTheme(ThemeItem theme) {
    ThemeConfig.initTheme(theme);
    notifyListeners();
  }
}

切换主题时,直接调用:

Provider.of<AppInfoProvider>(context, listen: false).setTheme(themeItem);

7. 创建一个主题仓库,里面存放两套主题,用于演示 Demo

///主题仓库
abstract class ThemeStore {
  static List<ThemeItem> themes = [
    //红色主题
    ThemeItem(
      ThemeEnum.yellow,
      ThemeData(
        primaryColor: Colors.yellow,
        backgroundColor: Colors.yellow,
      ),
      connerColor: Colors.blue,
    ),
    //黄色主题
    ThemeItem(
      ThemeEnum.red,
      ThemeData(
        primaryColor: Colors.red,
        backgroundColor: Colors.red,
      ),
      connerColor: Colors.green,
    ),
  ];
}

8. 好了,完事具备,完整的 demo 代码以及效果如下:

main.dart

void main() {
  ///初始化主题
  ThemeConfig.initTheme(
    ThemeStore.themes.first,
  );

  runApp(const Material(
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider.value(value: AppInfoProvider()),
      ],
      child: Consumer<AppInfoProvider>(
        builder: (context, appInfo, _) {
          return MaterialApp(
            theme: appInfo.currentTheme,
            home: Column(
              mainAxisSize: MainAxisSize.max,
              mainAxisAlignment: MainAxisAlignment.center,
              children: const [
                BodyWidget(),
                SizedBox(
                  height: 30,
                ),
                _ThemePageButton(),
              ],
            ),
          );
        },
      ),
    );
  }
}

class _ThemePageButton extends StatelessWidget {
  const _ThemePageButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => const ThemeSetWidget(),
          ),
        );
      },
      child: const Text('打开主题设置页面'),
    );
  }
}

body_widget

class BodyWidget extends StatelessWidget {
  const BodyWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 300,
      width: 300,
      //读取标准色值
      color: Theme.of(context).backgroundColor,
      child: Center(
        child: Container(
          height: 100,
          width: 150,
          //读取自定义色值
          color: Theme.of(context).connerColor,
        ),
      ),
    );
  }
}

两个方块,外层方块读取的 ThemeData 标注字段色值,内层方块读取扩展字段色值。统一通过 ThemeData 读取。

theme_set_widget

extension ExThemeEnum on ThemeEnum {
  Color get value {
    switch (this) {
      case ThemeEnum.yellow:
        return Colors.yellow;
      case ThemeEnum.red:
        return Colors.red;
    }
  }
}
///主题选择页面
class ThemeSetWidget extends StatelessWidget {
  const ThemeSetWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("颜色主题"),
        backgroundColor: Theme.of(context).backgroundColor,
      ),
      body: ExpansionTile(
        leading: const Icon(Icons.color_lens),
        title: const Text('颜色主题'),
        initiallyExpanded: true,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.only(
              left: 10,
              right: 10,
              bottom: 10,
            ),
            child: Wrap(
              spacing: 8,
              runSpacing: 8,
              children: ThemeStore.themes
                  .map((e) => _createItemWidget(context, e))
                  .toList(),
            ),
          )
        ],
      ),
    );
  }

  Widget _createItemWidget(
    BuildContext context,
    ThemeItem theme,
  ) {
    return InkWell(
      onTap: () {
        Provider.of<AppInfoProvider>(context, listen: false).setTheme(theme);
      },
      child: Container(
        width: 40,
        height: 40,
        color: theme.themeEnum.value,
        child: ThemeConfig.currentTheme == theme.themeEnum
            ? const Icon(
                Icons.done,
                color: Colors.white,
              )
            : null,
      ),
    );
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容