使用 Themes 统一颜色和字体风格

你可以使用主题来全局应用颜色和文字样式。

你可以定义应用全局的主题。你也可以为某一个组件单独继承一个特定的主题。每个主题都可以各自定义颜色、文字样式和其他 Material 配置参数。

Flutter 会按以下顺序应用样式:

  1. 针对特定 widget 的样式。

  2. 重载的继承主题的样式。

  3. 应用的总体样式。

在定义一个 Theme 之后,我们可以让它在指定的 widgets,包括 Flutter 自带的 Material widgets,例如 AppBars、Buttons、Checkboxes 等 widget 中生效。

Create an app theme

全局 Theme 会影响整个 app 的颜色和字体样式。只需要向 MaterialApp 构造器传入 ThemeData 即可。

从 Flutter 3.16 版本开始, Material 3 是 Flutter 的默认主题。

如果没有手动配置主题,Flutter 将会使用预设的样式。

MaterialApp(
  title: appName,
  theme: ThemeData(
    useMaterial3: true,

    // Define the default brightness and colors.
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.purple,
      // ···
      brightness: Brightness.dark,
    ),

    // Define the default `TextTheme`. Use this to specify the default
    // text styling for headlines, titles, bodies of text, and more.
    textTheme: TextTheme(
      displayLarge: const TextStyle(
        fontSize: 72,
        fontWeight: FontWeight.bold,
      ),
      // ···
      titleLarge: GoogleFonts.oswald(
        fontSize: 30,
        fontStyle: FontStyle.italic,
      ),
      bodyMedium: GoogleFonts.merriweather(),
      displaySmall: GoogleFonts.pacifico(),
    ),
  ),
  home: const MyHomePage(
    title: appName,
  ),
);

大部分 ThemeData 实例会设置以下两个属性。它们会影响大部分样式属性。

  1. colorScheme 定义了颜色。

  2. textTheme 定义了文字样式。

你可以在 ThemeData 文档中查看所有可自定义的颜色和字体样式。

应用指定的主题

要想应用你的主题,使用 Theme.of(context) 方法来指定 widget 的样式属性。其包括但不限于样式和颜色。

Theme.of(context) 会查询 widget 树,并返回其中最近的 Theme。所以他会优先返回我们之前定义过的一个独立的 Theme,如果找不到,它会返回全局主题。

在下面的例子中,Container 的颜色使用的就是指定主题(上层)的颜色。

Container(
  padding: const EdgeInsets.symmetric(
    horizontal: 12,
    vertical: 12,
  ),
  color: Theme.of(context).colorScheme.primary,
  child: Text(
    'Text with a background color',
    // ···
    style: Theme.of(context).textTheme.bodyMedium!.copyWith(
          color: Theme.of(context).colorScheme.onPrimary,
        ),
  ),
),

Override a theme

你可以用 Theme widget 嵌套想要改变主题的部分以进行主题重载。

以下是两种重载主题的方法:

  1. 构造一个不一样的 ThemeData 实例。

  2. 继承上层主题。

Set a unique ThemeData instance

如果不想从任何全局 Theme 继承样式,我们可以创建一个 ThemeData() 实例,然后把它传给 Theme widget:

Theme(
  // Create a unique theme with `ThemeData`.
  data: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.pink,
    ),
  ),
  child: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add),
  ),
);

Extend the parent theme

相比从头开始定义一套样式,从上层 Theme 扩展可能更常规一些,使用 copyWith() 方法即可。

Theme(
  // Find and extend the parent theme using `copyWith`.
  // To learn more, check out the section on `Theme.of`.
  data: Theme.of(context).copyWith(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.pink,
    ),
  ),
  child: const FloatingActionButton(
    onPressed: null,
    child: Icon(Icons.add),
  ),
);

观看 Theme 的相关视频

想要了解更多,你可以观看 Widget of the Week 中关于 Theme 的短视频:

交互式样例

import 'package:flutter/material.dart';
// Include the Google Fonts package to provide more text format options
// https://pub.dev/packages/google_fonts
import 'package:google_fonts/google_fonts.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    const appName = 'Custom Themes';

    return MaterialApp(
      title: appName,
      theme: ThemeData(
        useMaterial3: true,

        // Define the default brightness and colors.
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.purple,
          // TRY THIS: Change to "Brightness.light"
          //           and see that all colors change
          //           to better contrast a light background.
          brightness: Brightness.dark,
        ),

        // Define the default `TextTheme`. Use this to specify the default
        // text styling for headlines, titles, bodies of text, and more.
        textTheme: TextTheme(
          displayLarge: const TextStyle(
            fontSize: 72,
            fontWeight: FontWeight.bold,
          ),
          // TRY THIS: Change one of the GoogleFonts
          //           to "lato", "poppins", or "lora".
          //           The title uses "titleLarge"
          //           and the middle text uses "bodyMedium".
          titleLarge: GoogleFonts.oswald(
            fontSize: 30,
            fontStyle: FontStyle.italic,
          ),
          bodyMedium: GoogleFonts.merriweather(),
          displaySmall: GoogleFonts.pacifico(),
        ),
      ),
      home: const MyHomePage(
        title: appName,
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final String title;

  const MyHomePage({super.key, required this.title});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title,
            style: Theme.of(context).textTheme.titleLarge!.copyWith(
                  color: Theme.of(context).colorScheme.onSecondary,
                )),
        backgroundColor: Theme.of(context).colorScheme.secondary,
      ),
      body: Center(
        child: Container(
          padding: const EdgeInsets.symmetric(
            horizontal: 12,
            vertical: 12,
          ),
          color: Theme.of(context).colorScheme.primary,
          child: Text(
            'Text with a background color',
            // TRY THIS: Change the Text value
            //           or change the Theme.of(context).textTheme
            //           to "displayLarge" or "displaySmall".
            style: Theme.of(context).textTheme.bodyMedium!.copyWith(
                  color: Theme.of(context).colorScheme.onPrimary,
                ),
          ),
        ),
      ),
      floatingActionButton: Theme(
        data: Theme.of(context).copyWith(
          // TRY THIS: Change the seedColor to "Colors.red" or
          //           "Colors.blue".
          colorScheme: ColorScheme.fromSeed(
            seedColor: Colors.pink,
            brightness: Brightness.dark,
          ),
        ),
        child: FloatingActionButton(
          onPressed: () {},
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}