2、全局主题配置:在 MaterialApp 中配置全局主题,实现一键换肤的基础架构

好,咱们直接进入正题。上一章我们聊了为什么要做主题定制,这一章就来动手搭架子。

全局主题配置,说白了就是给整个 App 定一套「皮肤规则」。你想想看,如果每个页面都单独写颜色、写字体,那改起来得多痛苦?我早期做项目时就吃过这个亏——客户说「把主色调从蓝色改成绿色」,我改了整整两天,还漏了好几个按钮。嗯,从那以后,我再也不敢不搞全局主题了。

2.1 核心思路:ThemeData 就是你的调色盘

Flutter 里做主题,核心就一个类——ThemeData。它就像一个大容器,装着所有组件的样式:颜色、字体、形状、阴影……你只需要在 MaterialApp 里配置一次,整个 App 都能共享。

我个人习惯把主题配置拆成三块:

  • 颜色体系:主色、辅色、背景色、文字色
  • 文字体系:标题、正文、小字的字号和字重
  • 组件样式:按钮、输入框、卡片等具体控件的默认样式

这样拆的好处是——改颜色不影响文字,改文字不影响按钮,各管各的。

2.2 基础代码:在 MaterialApp 里挂主题

先看一个最基础的例子。你只需要在 MaterialApptheme 参数里传入一个 ThemeData 对象:

MaterialApp(
  title: '我的 App',
  theme: ThemeData(
    // 主色调
    primarySwatch: Colors.blue,
    // 视觉密度(紧凑还是宽松)
    visualDensity: VisualDensity.adaptivePlatformDensity,
  ),
  home: MyHomePage(),
);

这样写没问题,但太简单了。实际项目中,我们往往需要更精细的控制。我建议你直接定义 ThemeData 的各个属性,而不是只靠 primarySwatch

ThemeData(
  // 颜色
  primaryColor: Color(0xFF2196F3),
  colorScheme: ColorScheme.fromSeed(
    seedColor: Color(0xFF2196F3),
    brightness: Brightness.light,
  ),
  // 文字
  textTheme: TextTheme(
    headlineLarge: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
    bodyLarge: TextStyle(fontSize: 16, color: Colors.black87),
  ),
  // 组件
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ButtonStyle(
      backgroundColor: MaterialStateProperty.all(Color(0xFF2196F3)),
      foregroundColor: MaterialStateProperty.all(Colors.white),
      shape: MaterialStateProperty.all(
        RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
      ),
    ),
  ),
);
我的小技巧:colorScheme 替代 primarySwatch。前者更灵活,支持亮色/暗色模式切换,后者是旧 API,迟早要淘汰。

2.3 一键换肤的核心:多主题切换

一键换肤,说白了就是准备多套 ThemeData,然后动态切换。我常用的做法是定义一个主题数据类:

class AppTheme {
  static ThemeData lightTheme = ThemeData(
    brightness: Brightness.light,
    primaryColor: Color(0xFF2196F3),
    colorScheme: ColorScheme.light(
      primary: Color(0xFF2196F3),
      secondary: Color(0xFF03DAC6),
    ),
    // ... 其他配置
  );

  static ThemeData darkTheme = ThemeData(
    brightness: Brightness.dark,
    primaryColor: Color(0xFF90CAF9),
    colorScheme: ColorScheme.dark(
      primary: Color(0xFF90CAF9),
      secondary: Color(0xFF03DAC6),
    ),
    // ... 其他配置
  );
}

然后在 MaterialApp 里根据状态切换:

MaterialApp(
  theme: AppTheme.lightTheme,
  darkTheme: AppTheme.darkTheme,
  themeMode: ThemeMode.system, // 跟随系统,也可以手动指定
  home: MyHomePage(),
);

这里有个坑——themeMode 有三个值:lightdarksystem。如果你只想要亮色,就设 ThemeMode.light;想要暗色就设 ThemeMode.dark。我曾经犯过一个错:只配了 theme 没配 darkTheme,结果用户手机切到暗色模式时,App 直接崩了……

注意: 如果你同时配置了 themedarkTheme,一定要确保 themeMode 设置正确。否则系统自动切换时,可能找不到对应的主题配置。

2.4 进阶:用 InheritedWidget 实现全局主题状态

如果你需要更复杂的换肤逻辑(比如用户自定义颜色),光靠 MaterialApptheme 参数就不够了。这时候我推荐用 InheritedWidget 或者 Provider 来管理主题状态。

这里给一个简单的 InheritedWidget 示例:

class ThemeProvider extends InheritedWidget {
  final ThemeData themeData;
  final bool isDarkMode;

  const ThemeProvider({
    Key? key,
    required this.themeData,
    required this.isDarkMode,
    required Widget child,
  }) : super(key: key, child: child);

  static ThemeProvider? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
  }

  @override
  bool updateShouldNotify(ThemeProvider oldWidget) {
    return themeData != oldWidget.themeData || isDarkMode != oldWidget.isDarkMode;
  }
}

然后在顶层包裹:

ThemeProvider(
  themeData: currentTheme,
  isDarkMode: _isDarkMode,
  child: MaterialApp(
    theme: currentTheme,
    home: MyHomePage(),
  ),
);

这样,任何子组件都能通过 ThemeProvider.of(context) 拿到当前主题,甚至能触发重建。

核心要点总结:

  • ThemeData 是全局主题的基石,所有样式都从这里来
  • colorScheme 替代 primarySwatch,更现代更灵活
  • 亮色/暗色模式通过 theme + darkTheme + themeMode 三件套实现
  • 复杂场景用 InheritedWidget 或状态管理库来驱动主题切换

嗯,这一章的内容就到这。下一章我们会深入 ThemeData 的各个属性,手把手教你配出一套漂亮又实用的主题。到时候我会分享一些我在设计系统里踩过的坑,比如「为什么我的按钮颜色死活改不了」——你猜怎么着?八成是 ThemeData 的优先级没搞明白。