使用 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),
        ),
      ),
    );
  }
}