让你的应用界面好看的基石:Flutter主题Theme使用和扩展自定义字段

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

本文主要为两个部分:

  1. Flutter 主题基本应用
  2. 扩展主题自定义字段

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:

  • lightThemedarkTheme分别返回内置对象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字面意思:线性插值,用于在两个值之间进行平滑过渡,动画过渡上很有用,在这里主要是对齐内置ThemeDatalerp处理逻辑。

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

嵌入主题扩展只需要把主题扩展数据放入ThemeDataextensions字段里:

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 主题高效管理界面视觉属性,大幅提升开发效率,同时为用户带来舒适且独具魅力的视觉体验。

附:示例项目代码库: tinymahua/chatable: Chatable is an open source cross-platform table-data easy processing software written in Flutter and Rust.

相关推荐
weixin_3823952343 分钟前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__1 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.1 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~2 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华3 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子5 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅6 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni6 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学7 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端