flutter中如何使用和扩展ThemeData实现详解

前言

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

例如这样:

  • 定义常量存放
/// 存放颜色常量
abstract class ColorConfigs {
 static const Color background = Color(0xFFFF6600);
 static const Color textHint = Color(0xFFA0A4A7);
}
  • 通过统一获取
/// 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 进行一层封装处理

我们对 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进行读取保持统一

为保持统一通过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,
 ),
 );
 }
}

作者:李小轰_Rex

%s 个评论

要回复文章请先登录注册