Kodokon kodokon.com

Темы: ThemeData, Material 3 и тёмный режим

Собери цвета и типографику в ThemeData, сгенерируй палитру Material 3 через ColorScheme.fromSeed и предложи тёмный режим без лишних усилий.

9 мин · 3 вопросов

Открыть этот урок в Kodokon

Перестать оформлять виджет за виджетом - это профессиональная веха: ThemeData собирает цвета, типографику и формы для всего приложения в одном месте. С Material 3 (включён по умолчанию начиная с Flutter 3.16) всё начинается с ColorScheme: ColorScheme.fromSeed выводит полную, гармоничную и доступную палитру из одного исходного цвета (seed).

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

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

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

  @override
  Widget build(BuildContext context) {
    const seed = Colors.teal;
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: seed,
        ),
      ),
      darkTheme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: seed,
          brightness: Brightness.dark,
        ),
      ),
      themeMode: ThemeMode.system,
      home: Scaffold(
        appBar: AppBar(title: const Text('Shop')),
        body: const Center(child: Text('Welcome')),
      ),
    );
  }
}
Две темы, сгенерированные из одного исходного цвета.

Внутри своих виджетов ничего не изобретай заново: читай окружающую тему через Theme.of(context). colorScheme предоставляет семантические роли (primary, onPrimary, primaryContainer, surface и так далее), а textTheme - именованные стили (titleMedium, bodyLarge и так далее). Написанный так виджет автоматически подстраивается и под светлую тему, и под тёмную.

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

class PriceTag extends StatelessWidget {
  const PriceTag({super.key, required this.price});

  final double price;

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    final text = Theme.of(context).textTheme;
    return Container(
      padding: const EdgeInsets.symmetric(
        horizontal: 12,
        vertical: 6,
      ),
      decoration: BoxDecoration(
        color: colors.primaryContainer,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Text(
        '$price €',
        style: text.titleMedium?.copyWith(
          color: colors.onPrimaryContainer,
        ),
      ),
    );
  }
}
Ни одного жёстко заданного цвета: виджет следует активной теме.

Чтобы пойти дальше, настрой темы компонентов: каждое семейство виджетов (панели приложения, кнопки, поля ввода) принимает свою тему в ThemeData. Ты один раз задаёшь отступы кнопок или центрирование заголовков, и каждый экземпляр наследует это без единого локального стиля.

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

final ThemeData appTheme = ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.teal,
  ),
  appBarTheme: const AppBarTheme(
    centerTitle: true,
    elevation: 0,
  ),
  filledButtonTheme: FilledButtonThemeData(
    style: FilledButton.styleFrom(
      padding: const EdgeInsets.symmetric(
        horizontal: 24,
        vertical: 12,
      ),
    ),
  ),
);
Настройки компонентов применяются во всём приложении.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Что создаёт ColorScheme.fromSeed(seedColor: ...)?
    • Один-единственный, слегка осветлённый цвет
    • Полную согласованную палитру Material 3, выведенную из исходного цвета
    • Градиент для фонов
    • Только тёмную тему
  2. Как заставить приложение следовать системной настройке светлой и тёмной темы?
    • Указать только theme, остальное произойдёт автоматически
    • Указать theme и darkTheme и задать themeMode как ThemeMode.system
    • Вызывать setState при смене яркости
    • Читать MediaQuery в каждом виджете
  3. Почему стоит писать color: Theme.of(context).colorScheme.primary, а не color: Colors.blue?
    • Так короче писать
    • Цвет подстраивается под активную тему и остаётся согласованным во всём приложении
    • Colors.blue объявлен устаревшим в Material 3