想让 APP 界面好看又吸引人,就像盖一栋漂亮的大楼一样。 在 Flutter 里,主题(Theme)就好比大楼的地基,它决定了整个 APP 的外观风格。 今天就带大家看看,怎么用好 Flutter 的主题,还能自己添加个性化设置,让你的 APP 颜值直接拉满!

本文主要为两个部分:
- Flutter 主题基本应用
- 扩展主题自定义字段
01. Flutter 主题:搭建界面的基础框架
为什么需要主题Theme? 作为图形界面程序,呈现出的每个元素都带有各种各样的视觉属性, 从按钮的颜色、形状,到文字的字体、大小,再到卡片的阴影效果,散落在程序的每个角落。如果没有一套高效的管理机制,这些视觉属性就如同城市里随意堆放的建筑材料,不仅会让开发者在维护和修改界面时手忙脚乱,还会导致应用界面风格混乱,影响用户体验。而主题Theme,正是解决这一难题的 "施工总指挥"。
a. 创建基础主题数据
全局 Theme 会影响整个 app 的颜色和字体样式。只需要向 MaterialApp
构造器传入 ThemeData
即可。 如果没有手动配置主题,Flutter 将会使用预设的样式。
从代码中可以看到,ThemeData
对象是在MaterialApp
应用初始化时传入,顾名思义,作为应用入口后续交互界面都共用这一份Theme定义。
less
MaterialApp(
title: appName,
theme: ThemeData(
// 定义颜色亮度等信息
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.purple,
// ···
brightness: Brightness.dark,
),
// ...其他字段
),
home: const MyHomePage(title: appName),
);
大部分 ThemeData
实例会设置colorScheme
属性,它会影响大部分颜色样式属性。
可以在 ThemeData
文档中查看所有可自定义的颜色和字体样式,从字段数量就能看出是个相当庞杂的对象结构,因此建议编写一个独立ThemeData管理类来汇总整个App的界面控制信息。 以下是一个示例:
类名是可以自定义的,从代码中可以看到,主要定义了明亮和黑暗模式两套主题。 使用静态常量主要是方便静态值的多次引用,然后分别是两个getter
:
lightTheme
和darkTheme
分别返回内置对象ThemeData
,初始化引用对应静态常量值。
可以看出
Flutter
考虑了相当多的界面视觉控制场景,但实际项目中仍然会有找不到所需场景字段的情况,此时就需要对内置主题进行扩展,这部分放到后文中讲。
scss
class ChatableThemeData {
static double dividerThickness = 6.0;
static double iconSize = 21.0;
static Color lightBorderColor = const Color(0xffe6e6e6);
static Color lightSurfaceColor = const Color(0xffffffff);
static Color lightOnSurfaceColor = Colors.black87;
static Color lightCanvasColor = const Color(0xfff9f9f9);
static Color lightOnCanvasColor = Colors.black26;
static Color lightPrimaryColor = Colors.blueAccent[700]!;
static Color lightOnPrimaryColor = Colors.white;
static Color lightError = Colors.red;
static Color lightOnError = Colors.white;
static Brightness lightBrightness = Brightness.light;
static Color lightToolIconColor = const Color(0xff878787);
static Color darkBorderColor = const Color(0xff3b3b3b);
static Color darkSurfaceColor = const Color(0xff262626);
static Color darkOnSurfaceColor = Colors.white;
static Color darkCanvasColor = const Color(0xff212121);
static Color darkOnCanvasColor = Colors.white54;
static Color darkPrimaryColor = Colors.blueAccent[700]!;
static Color darkOnPrimaryColor = Colors.black;
static Color darkError = Colors.red;
static Color darkOnError = Colors.white;
static Brightness darkBrightness = Brightness.dark;
static Color darkToolIconColor = Colors.white60;
static ThemeData get lightTheme {
return ThemeData(
fontFamily: 'NotoSansSC',
appBarTheme: AppBarTheme(
backgroundColor: lightCanvasColor,
foregroundColor: lightOnCanvasColor,
iconTheme: IconThemeData(color: lightToolIconColor, size: iconSize),
),
bottomAppBarTheme: BottomAppBarTheme(
color: lightCanvasColor
),
dividerTheme: DividerThemeData(color: lightCanvasColor, thickness: dividerThickness),
scaffoldBackgroundColor: lightSurfaceColor,
colorScheme: ColorScheme(
brightness: ChatableThemeData.lightBrightness,
primary: ChatableThemeData.lightPrimaryColor,
onPrimary: ChatableThemeData.lightOnPrimaryColor,
secondary: ChatableThemeData.lightPrimaryColor,
onSecondary: ChatableThemeData.lightOnPrimaryColor,
error: ChatableThemeData.lightError,
onError: ChatableThemeData.lightOnError,
surface: ChatableThemeData.lightSurfaceColor,
onSurface: lightOnSurfaceColor,
surfaceContainer: lightSurfaceColor,
),
);
}
static ThemeData get darkTheme {
return ThemeData(
fontFamily: 'NotoSansSC',
appBarTheme: AppBarTheme(
backgroundColor: darkCanvasColor,
foregroundColor: darkOnCanvasColor,
iconTheme: IconThemeData(color: darkToolIconColor, size: iconSize),
),
bottomAppBarTheme: const BottomAppBarTheme(
color: Color(0xff262626),
),
dividerTheme: DividerThemeData(color: darkCanvasColor, thickness: dividerThickness),
scaffoldBackgroundColor: darkSurfaceColor,
colorScheme: ColorScheme(
brightness: ChatableThemeData.darkBrightness,
primary: ChatableThemeData.darkPrimaryColor,
onPrimary: ChatableThemeData.darkOnPrimaryColor,
secondary: ChatableThemeData.darkPrimaryColor,
onSecondary: ChatableThemeData.darkOnPrimaryColor,
error: ChatableThemeData.darkError,
onError: ChatableThemeData.darkOnError,
surface: ChatableThemeData.darkSurfaceColor,
onSurface: darkOnSurfaceColor,
surfaceContainer: darkSurfaceColor,
),
// ...
);
}
}
b. 使用主题样式
主题Theme使用就简单多了,只需要在应用入口MaterialApp
对象初始化时传入自定义的ThemeData
数据即可:
应用初始化接受指定两种主题模式的ThemeData
数据,以及默认采用的主题: themeMode
可选值,ThemeMode
是个枚举: ThemeMode.system
: 自动模式,跟随系统主题设置 ThemeMode.light
: 明亮模式,会应用MaterialApp
初始化时theme
参数传入的值,例子中为ChatableThemeData.lightTheme
ThemeMode.dark
: 暗黑模式,会应用MaterialApp
初始化时darkTheme
参数传入的值,例子中为ChatableThemeData.darkTheme
less
return MaterialApp(
//
theme: ChatableThemeData.lightTheme,
darkTheme: ChatableThemeData.darkTheme,
themeMode: themeMode,
// ... 其他属性
);
之后在具体需要使用主题的组件中,可以用Theme.of(context)
来调用之前定义的Theme数据中的字段值:、
代码例子中是一个窗口最小化按钮,引用了appBarTheme
中的属性值
Theme.of(context)
是一个全局可调用对象,但有一个地方需要注意,一定要在组件build
方法中才能使用,否则会触发组件生命周期检测错误。 也就是不能在initState
或在组件类字段声明时用Theme.of(context)`赋默认值。
less
IconButton(
alignment: Alignment.center,
icon: Icon(
Icons.horizontal_rule_rounded,
size: Theme.of(context).appBarTheme.iconTheme!.size,
color: Theme.of(context).appBarTheme.iconTheme!.color!,
),
onPressed: () {
windowManager.minimize();
},
),
02. 扩展自定义字段:为界面增添独特设计
为什么要扩展自定义字段? 首先要明确一下,默认ThemeData
里的字段都是和Flutter应用内置样式息息相关,使用时需要注意弄清楚每个字段控制哪些地方,要不很有可能发生一个组件样式正常,另一个反而异常的情况。 而当发现自己所需场景在ThemeData
里找不到合适的字段的时候,切记不能随便找一个看起来用处不大的字段赋上值然后就在组件中使用,这样做会导致有可能在不确定的地方给你诡异的惊喜。
好在ThemeData
提供了一个extensions
字段,可以把一个或多个自定义的主题数据对象传入,然后组件中用Theme.of(context).extension<CustomTheme>()!.customThemeField
来使用。
笔者项目中遇到的情况是需要使用一个特殊的边框颜色,为避免污染内置属性里的边框样式,需要对ThemeData
进行扩展。 接下来就以这个案例来讲解如何使用这个方法。
a. 创建自定义主题扩展
首先定义一个自定义类ChatableColors
,继承ThemeExtension<ChatableColors>
:
如同内置
ThemeData
,分别定义明亮和暗黑两种模式需要的颜色值 声明一个borderColor
字段 为两种主题模式分别提供对应getter
除了这几个基本内容,两个继承覆盖父类的方法比较特殊:
copyWith
可以在getter
获取的主题数据基础上对部分字段进行二次设置,提供了更多灵活性
lerp
字面意思:线性插值,用于在两个值之间进行平滑过渡,动画过渡上很有用,在这里主要是对齐内置ThemeData
的lerp
处理逻辑。
scss
class ChatableColors extends ThemeExtension<ChatableColors> {
static Color lightBorderColor = const Color(0xffe6e6e6);
static Color darkBorderColor = const Color(0xff3b3b3b);
final Color borderColor;
ChatableColors({required this.borderColor});
static ChatableColors get light {
return ChatableColors(borderColor: lightBorderColor);
}
static ChatableColors get dark {
return ChatableColors(borderColor: darkBorderColor);
}
@override
ChatableColors copyWith({Color? withBorderColor}) {
return ChatableColors(borderColor: withBorderColor ?? borderColor);
}
@override
ChatableColors lerp(ThemeExtension<ChatableColors>? other, double t) {
if (other is! ChatableColors) {
return this;
}
return ChatableColors(
borderColor: Color.lerp(borderColor, other.borderColor, t)?? borderColor,
);
}
}
b. 将主题扩展嵌入内置ThemeData
嵌入主题扩展只需要把主题扩展数据放入ThemeData
的extensions
字段里:
swift
class ChatableThemeData {
// 省略之前代码
static ThemeData get lightTheme {
return ThemeData(
// 省略之前代码
extensions: <ThemeExtension<dynamic>>[
ChatableColors.light,
],
);
}
static ThemeData get darkTheme {
return ThemeData(
// 省略之前代码
extensions: <ThemeExtension<dynamic>>[
ChatableColors.dark,
],
// ...
);
}
}
c. 组件中使用自定义主题扩展字段值
类似内置主题字段使用,代码如下:
extension<ChatableColors>()
用泛型参数告诉Theme.of(context)
对象要取的主题扩展对象为ChatableColors
。 由于是自定义主题扩展,获取扩展对象方法有可能返回空,因此需要加感叹号处理空安全的问题。
css
Container(
color: Theme.of(context).extension<ChatableColors>()!.borderColor,
);
03. 总结
以上就是Flutter主题应用及扩展字段方法,掌握这些技巧,意味着开发者拥有了打造高品质应用界面的 "魔法钥匙"。 无论是小型工具类应用,还是功能复杂的大型项目,都能借助 Flutter 主题高效管理界面视觉属性,大幅提升开发效率,同时为用户带来舒适且独具魅力的视觉体验。