0Pricing
Flutter Mobile Development · 课时

Material 3 配色方案与种子颜色

使用 ColorScheme.fromSeed 从单个种子颜色生成协调的配色方案。

Material 3 配色方案与种子颜色 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

What Is a Color Scheme in Material 3?

Material 3 (M3) introduced a structured color scheme — a set of named color roles that your entire app uses consistently. Instead of scattering raw hex values across widgets, you define roles like primary, onPrimary, surface, error, and Flutter's theming engine applies them automatically.

  • primary — the brand color used for buttons, FABs, active tabs
  • onPrimary — text/icons drawn on top of primary
  • surface — background of cards, sheets, dialogs
  • onSurface — text drawn on surfaces
  • error / onError — validation and alert colors

Every role comes in both a light and a dark variant, letting you support both themes with one color scheme definition.

The Problem ColorScheme.fromSeed Solves

Before M3, developers had to hand-pick every color role — primary, secondary, tertiary, surfaces, containers, and their 'on' counterparts. That's 25+ values, and getting accessible contrast ratios right manually is error-prone.

ColorScheme.fromSeed solves this by accepting a single seed color and using Google's HCT color space algorithm (Hue, Chroma, Tone) to generate a full, harmonious, WCAG-accessible palette automatically.

  • One input → 25+ output roles
  • Guaranteed contrast between foreground and background pairs
  • Light and dark variants from the same seed
  • Consistent with the Material You design language

Your First ColorScheme.fromSeed

Using ColorScheme.fromSeed is straightforward. Pass it to ThemeData inside your MaterialApp. Flutter does the rest.

The seedColor parameter accepts any Color — typically your brand color. Flutter then derives the entire palette from it.

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'M3 Theming Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: const Color(0xFF6750A4), // brand purple
        ),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Color Scheme Demo')),
      body: const Center(child: Text('Hello Material 3')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        child: const Icon(Icons.palette),
      ),
    );
  }
}

Light and Dark from the Same Seed

One of the biggest wins of ColorScheme.fromSeed is that you can generate both a light and a dark theme from the same seed color, ensuring visual consistency across modes.

Pass the brightness parameter to switch between them. Flutter adjusts tones automatically — dark themes use lighter tones of your seed on dark surfaces.

import 'package:flutter/material.dart';

const Color _brandColor = Color(0xFF00897B); // teal brand

ThemeData lightTheme() => ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: _brandColor,
        brightness: Brightness.light,
      ),
      useMaterial3: true,
    );

ThemeData darkTheme() => ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: _brandColor,
        brightness: Brightness.dark,
      ),
      useMaterial3: true,
    );

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: lightTheme(),
      darkTheme: darkTheme(),
      themeMode: ThemeMode.system, // follows device setting
      home: const Scaffold(
        body: Center(child: Text('Adaptive theme')),
      ),
    );
  }
}

Accessing Color Roles in Widgets

Once your ColorScheme is set up, access its roles anywhere via Theme.of(context).colorScheme. This keeps your widgets decoupled from hard-coded colors and automatically respects light/dark mode.

Common roles you will use in widgets:

  • colorScheme.primary — highlight color for interactive elements
  • colorScheme.onPrimary — text/icon color on primary backgrounds
  • colorScheme.surface — card and dialog backgrounds
  • colorScheme.onSurface — default text color
  • colorScheme.error — error state indicators
import 'package:flutter/material.dart';

class StatusBadge extends StatelessWidget {
  final String label;
  final bool isActive;

  const StatusBadge({super.key, required this.label, required this.isActive});

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      decoration: BoxDecoration(
        color: isActive ? cs.primaryContainer : cs.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(20),
      ),
      child: Text(
        label,
        style: TextStyle(
          color: isActive ? cs.onPrimaryContainer : cs.onSurfaceVariant,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }
}

Container Roles: primaryContainer and Friends

M3 introduced container roles — softer, lower-chroma versions of core colors designed for backgrounds of UI elements like chips, cards, and filled buttons.

  • primaryContainer — soft primary background (great for selected chips, filled tonal buttons)
  • onPrimaryContainer — text on top of primaryContainer
  • secondaryContainer / tertiaryContainer — analogous roles for secondary and tertiary palettes
  • errorContainer — soft red for inline error banners

Using containers instead of raw primary for backgrounds gives you visual hierarchy and keeps contrast ratios safe without manually adjusting opacity.

import 'package:flutter/material.dart';

class InfoCard extends StatelessWidget {
  final String title;
  final String body;

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

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;

    return Card(
      color: cs.primaryContainer,
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              title,
              style: Theme.of(context).textTheme.titleMedium?.copyWith(
                    color: cs.onPrimaryContainer,
                    fontWeight: FontWeight.bold,
                  ),
            ),
            const SizedBox(height: 8),
            Text(
              body,
              style: TextStyle(color: cs.onPrimaryContainer),
            ),
          ],
        ),
      ),
    );
  }
}

Overriding Specific Roles

Sometimes the algorithmically generated color for a specific role doesn't match your brand guide exactly. You can override individual roles by using ColorScheme.fromSeed(...).copyWith(...). This keeps all the auto-generated roles while letting you pin specific ones.

This pattern is useful when, for example, your design system requires an exact red for error that differs from what the HCT algorithm produces.

import 'package:flutter/material.dart';

ThemeData buildTheme() {
  // Start from seed, then pin specific roles
  final base = ColorScheme.fromSeed(
    seedColor: const Color(0xFF1565C0), // deep blue brand
  );

  return ThemeData(
    colorScheme: base.copyWith(
      // Exact error red from our design system
      error: const Color(0xFFB00020),
      onError: Colors.white,
      // Pin tertiary to our accent orange
      tertiary: const Color(0xFFE65100),
      onTertiary: Colors.white,
    ),
    useMaterial3: true,
  );
}

Dynamic Color: Adapting to Wallpaper

Android 12+ supports Dynamic Color — the system extracts a seed color from the user's wallpaper and applies it to all apps. Flutter supports this via the dynamic_color package.

The pattern is: try to load the system color scheme; fall back to your own seed if unavailable (iOS, older Android, or when the user hasn't set a wallpaper).

This makes your app feel native and personalized on supported devices while remaining consistent everywhere else.

// pubspec.yaml: dynamic_color: ^1.7.0

import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';

const Color _fallbackSeed = Color(0xFF6750A4);

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

  @override
  Widget build(BuildContext context) {
    return DynamicColorBuilder(
      builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
        final lightScheme = lightDynamic ??
            ColorScheme.fromSeed(
              seedColor: _fallbackSeed,
              brightness: Brightness.light,
            );

        final darkScheme = darkDynamic ??
            ColorScheme.fromSeed(
              seedColor: _fallbackSeed,
              brightness: Brightness.dark,
            );

        return MaterialApp(
          theme: ThemeData(colorScheme: lightScheme, useMaterial3: true),
          darkTheme: ThemeData(colorScheme: darkScheme, useMaterial3: true),
          themeMode: ThemeMode.system,
          home: const Scaffold(
            body: Center(child: Text('Dynamic Color')),
          ),
        );
      },
    );
  }
}

Previewing Your Palette with a Color Swatch Grid

While developing, it's useful to render all key color roles on-screen so you can verify the generated palette visually. This debug screen is a quick way to check contrast and harmony before shipping.

Build a simple grid that maps role names to their colors and 'on' counterparts, using the live ColorScheme from context.

import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;

    final roles = [
      ('Primary', cs.primary, cs.onPrimary),
      ('PrimaryContainer', cs.primaryContainer, cs.onPrimaryContainer),
      ('Secondary', cs.secondary, cs.onSecondary),
      ('SecondaryContainer', cs.secondaryContainer, cs.onSecondaryContainer),
      ('Tertiary', cs.tertiary, cs.onTertiary),
      ('Surface', cs.surface, cs.onSurface),
      ('Error', cs.error, cs.onError),
      ('ErrorContainer', cs.errorContainer, cs.onErrorContainer),
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('Color Palette')),
      body: ListView(
        children: roles.map((r) {
          final (name, bg, fg) = r;
          return Container(
            height: 56,
            color: bg,
            alignment: Alignment.centerLeft,
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Text(name, style: TextStyle(color: fg, fontWeight: FontWeight.w500)),
          );
        }).toList(),
      ),
    );
  }
}

Common Pitfalls with Color Schemes

Avoid these mistakes when working with M3 color schemes:

  • Hard-coding hex colors in widgets — Always use Theme.of(context).colorScheme.* so widgets respond to theme changes and dark mode.
  • Using Colors.white / Colors.black directly — These break in dark mode. Use colorScheme.onSurface or colorScheme.surface instead.
  • Forgetting useMaterial3: true — Without this flag, Flutter uses M2 defaults and your M3 color roles may not be applied correctly to built-in widgets.
  • Using primaryColor from ThemeData — That's an M2 concept. In M3, all widgets read from colorScheme. Set ThemeData.colorScheme, not ThemeData.primaryColor.
  • Mismatching brightness and background — Generating a Brightness.dark scheme but pairing it with a white scaffold will produce unreadable UI.

Putting It All Together: A Themed App Shell

Here is a complete minimal app shell that demonstrates best practices: seed-based color scheme, light/dark support, useMaterial3: true, and widgets that read roles from context — no hard-coded colors anywhere.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

const Color _seed = Color(0xFF2E7D32); // forest green brand

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Green Theme',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: _seed,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: _seed,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      themeMode: ThemeMode.system,
      home: const HomeScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final cs = Theme.of(context).colorScheme;
    return Scaffold(
      backgroundColor: cs.surface,
      appBar: AppBar(
        title: Text('Dashboard', style: TextStyle(color: cs.onPrimary)),
        backgroundColor: cs.primary,
      ),
      body: Center(
        child: FilledButton.icon(
          onPressed: () {},
          icon: const Icon(Icons.eco),
          label: const Text('Go Green'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        backgroundColor: cs.tertiary,
        foregroundColor: cs.onTertiary,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Quick Check: ColorScheme.fromSeed

Test your understanding of how ColorScheme.fromSeed works in Material 3.

Lesson Recap: Material 3 Color Schemes and Seed Colors

Here's what you learned in this lesson:

  • ColorScheme in M3 defines a full set of named color roles (primary, surface, error, containers, and their 'on' counterparts) that Flutter widgets consume automatically.
  • ColorScheme.fromSeed generates all 25+ roles from a single brand color using Google's HCT algorithm, guaranteeing harmony and accessibility.
  • Pass brightness: Brightness.light or Brightness.dark to generate appropriate variants from the same seed — always pair with MaterialApp.darkTheme for full dark mode support.
  • Access roles in widgets via Theme.of(context).colorScheme — never hard-code hex values in widget files.
  • Use container roles (primaryContainer, onPrimaryContainer) for backgrounds of UI components; they provide visual hierarchy without manual opacity tweaks.
  • Override individual roles with .copyWith() when a specific brand color must be exact.
  • On Android 12+, Dynamic Color via the dynamic_color package extracts the system seed; always provide a fallback for other platforms.
  • Always set useMaterial3: true on ThemeData so built-in widgets read from the M3 color scheme correctly.

常见问题解答

「Material 3 配色方案与种子颜色」课时是免费的吗?

是的 — 「Material 3 配色方案与种子颜色」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。

「Material 3 配色方案与种子颜色」这节课中我会学到什么?

使用 ColorScheme.fromSeed 从单个种子颜色生成协调的配色方案。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Flutter Mobile Development 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「Material 3 配色方案与种子颜色」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Flutter Mobile Development 课中编写并运行代码吗?

能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. Material 3 配色方案与种子颜色
  2. 动态颜色与自适应明暗主题
  3. 用于品牌令牌的自定义 ThemeExtension
  4. 响应式排版与组件主题
← 返回 Flutter Mobile Development