Irbisa · Шпаргалка

Темизация и дизайн-система

ThemeData, ColorScheme.fromSeed, Material 3, ThemeExtension, шрифты, тёмная тема

Middle-разработчик · 22 вопроса

  1. 01

    Как задать тему Flutter-приложению через ThemeData и как виджет её читает?

    Вы передаёте ThemeData в MaterialApp(theme: ...), и любой виджет ниже читает её через Theme.of(context).

    class App extends StatelessWidget {  const App({super.key});   @override  Widget build(BuildContext context) {    return MaterialApp(…
  2. 02

    Что делает ColorScheme.fromSeed и почему итоговый primary не совпадает с вашим брендовым цветом?

    fromSeed считает ваш цвет семенем: строит из его оттенка тональные палитры и берёт каждую роль (primary, surface, error…) с фиксированного тона, поэтому любой цвет и его пара on читаемы по построению.

    const brand = Color(0xFF00695C); final scheme = ColorScheme.fromSeed(seedColor: brand);// scheme.primary != brand: it is the palette tone that reads well on surface // When the literal brand colour is required…
  3. 03

    Что такое TextTheme и зачем пользоваться его ролями вместо TextStyle, написанного на каждом экране?

    TextTheme хранит набор именованных стилей текста внутри ThemeData, 15 ролей от displayLarge до labelSmall.

    // before: one decision copied into every screenText('Monthly total',    style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w600)); // after: a role the theme ownsText('Monthly total', style: Theme.of(context).textTheme.titleLarge);…
  4. 04

    Как добавить во Flutter-приложение свои шрифты и свои иконки?

    Положите файлы шрифта в проект, объявите их в pubspec.yaml в разделе flutter: fonts: и используйте имя семейства в ThemeData(fontFamily: ...) или в TextStyle.

    # pubspec.yamlflutter:  uses-material-design: true   # the font behind Icons.*   fonts:    - family: Inter…
  5. 05

    Чем отличаются Material- и Cupertino-виджеты и можно ли использовать Cupertino-виджеты внутри MaterialApp?

    Material-виджеты следуют Material Design от Google и настраиваются через ThemeData; Cupertino-виджеты повторяют контролы iOS и настраиваются через CupertinoThemeData.

  6. 06

    Как поддержать светлую и тёмную тему через theme, darkTheme и themeMode?

    Передайте в MaterialApp и theme, и darkTheme, а выбор оставьте themeMode: ThemeMode.system следует настройке устройства, light и dark включают одну из тем принудительно.

    const _brand = Color(0xFF6750A4); ThemeData buildTheme(Brightness brightness) {  final cs = ColorScheme.fromSeed(seedColor: _brand, brightness: brightness);  return ThemeData(    colorScheme: cs, // brightness comes from the scheme…
  7. 07

    Найдите баг: на некоторых телефонах белый логотип невидим на AppBar, хотя приложение находится в светлой теме.

    class BrandLogo extends StatelessWidget {  const BrandLogo({super.key});   @override  Widget build(BuildContext context) {    final isDark =        MediaQuery.platformBrightnessOf(context) == Brightness.dark;    return Image.asset(      isDark ? 'assets/logo_white.png' : 'assets/logo_black.png',      height: 24,    );  }}

    MediaQuery.platformBrightnessOf сообщает системную настройку телефона, а не тему, которой рисует приложение.

  8. 08

    Этот виджет строит MaterialApp с брендовой темой, но прямоугольник закрашен дефолтным фиолетовым. Почему?

    class App extends StatelessWidget {  const App({super.key});   @override  Widget build(BuildContext context) {    final primary = Theme.of(context).colorScheme.primary;    return MaterialApp(      theme: ThemeData(        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF00695C)),      ),      home: ColoredBox(color: primary, child: const HomePage()),    );  }}

    Theme.of(context) ищет тему вверх по дереву от переданного контекста, а этот контекст принадлежит App, который стоит выше построенного им же MaterialApp.

  9. 09

    Найдите баг: после переключения на тёмную тему карточка становится тёмной, а цена на ней остаётся цвета светлой темы.

    class _PriceTagState extends State<PriceTag> {  late final TextStyle _priceStyle = Theme.of(context)      .textTheme      .headlineSmall!      .copyWith(color: Theme.of(context).colorScheme.primary);   @override  Widget build(BuildContext context) {    return Card(      child: Padding(        padding: const EdgeInsets.all(16),        child: Text(widget.price, style: _priceStyle),      ),    );  }}

    Стиль закэширован в поле late final, инициализатор которого выполняется один раз, при первом build.

  10. 10

    В этом блоке хотели только красные кнопки, но текст потерял шрифт приложения, а обращение к ThemeExtension теперь возвращает null. Что пошло не так?

    @overrideWidget build(BuildContext context) {  return Theme(    data: ThemeData(      colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),    ),    child: const DangerZone(), // buttons, text, BrandTokens.of(context)  );}

    Theme(data: ThemeData(...)) не дополняет тему поддерева, а заменяет её целиком: у новой ThemeData дефолтный шрифт, дефолтные темы компонентов и нет расширений.

  11. 11

    _canPay равен false, но кнопка всё равно закрашена брендовым цветом. Почему и как Material-кнопка определяет свой стиль?

    const brand = Color(0xFF00695C); FilledButton(  onPressed: _canPay ? _pay : null,  style: const ButtonStyle(    backgroundColor: WidgetStatePropertyAll(brand),  ),  child: const Text('Pay'),);

    WidgetStatePropertyAll(brand) отвечает brand на любое состояние, включая disabled, а значение из style побеждает и тему компонента, и дефолты кнопки, поэтому цвет выключенного состояния из дефолтов просто не получает слова.

  12. 12

    В дизайне Inter с весами 400, 500, 600 и 700, но на устройстве несколько весов выглядят одинаково. Что не так с настройкой шрифтов?

    Flutter рисует только те веса, файлы которых объявлены в pubspec.yaml.

    # pubspec.yaml: every weight the design uses is a declared fileflutter:  fonts:    - family: Inter      fonts:        - asset: assets/fonts/Inter-Regular.ttf    # weight 400 by default…
  13. 13

    Что такое дизайн-токены и как примитивные и семантические токены ложатся на тему Flutter?

    Под дизайн-токенами понимают именованные решения дизайн-системы: цвета, стили текста, отступы, радиусы, хранящиеся как данные, а не как литералы.

  14. 14

    Что такое ThemeExtension и как добавить в ThemeData свои токены, например цвет успеха или шаг отступа?

    ThemeExtension<T> даёт встроенный способ хранить свои токены внутри ThemeData: вы наследуетесь от него, регистрируете экземпляры в ThemeData.extensions и читаете их через Theme.of(context).extension<T>(), и тогда они переключаются и анимируются вместе с темой, как встроенные цвета.

    @immutableclass BrandTokens extends ThemeExtension<BrandTokens> {  const BrandTokens({required this.success, required this.gutter});   final Color success;  final double gutter;…
  15. 15

    Что меняется при переходе приложения с Material 2 на Material 3 и что ломается так, что никто не замечает?

    Material 3, дефолт с Flutter 3.16, красит каждый компонент ролями ColorScheme вместо primaryColor, разделяет слои тоном поверхности вместо тени и меняет типографику, формы и ряд виджетов.

  16. 16

    Как тема MaterialApp доходит до Cupertino-виджетов и когда нужен cupertinoOverrideTheme?

    Каждый Theme заодно предоставляет CupertinoTheme (MaterialBasedCupertinoThemeData), которая берёт из ThemeData лишь несколько значений: яркость, основной цвет, контрастный к нему цвет и фон scaffold (плюс цвет ручек выделения).

    final theme = ThemeData(  colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF0B6E4F)),  fontFamily: 'Inter',  // Mainly brightness, primaryColor, primaryContrastingColor and  // scaffoldBackgroundColor are derived from the Material theme above.  cupertinoOverrideTheme: const CupertinoThemeData(…
  17. 17

    Как поддержать динамические цвета Material You на Android, сохранив брендовый запасной вариант, и когда от них стоит отказаться?

    Встроенной палитры обоев во Flutter нет: её читают через DynamicColorBuilder из пакета dynamic_color, используют эти схемы, когда они не null, а во всех остальных случаях откатываются на ColorScheme.fromSeed(seedColor: brandSeed).

    import 'package:dynamic_color/dynamic_color.dart'; const _brandSeed = Color(0xFF6750A4); class App extends StatelessWidget {  const App({super.key});…
  18. 18

    Когда дизайн-системе стоит завести собственный InheritedWidget вместо того, чтобы класть значения в ThemeExtension, и чем платят в каждом варианте?

    Берите ThemeExtension, когда значение по природе относится к теме и должно интерполироваться вместе с ней, и свой InheritedWidget, когда это не так: другое время жизни, другая частота изменений или значения, которым в ThemeData делать нечего.

  19. 19

    Как MaterialApp анимирует смену темы и почему переключение в тёмный режим может моргнуть белым или заставить градиент прыгнуть?

    MaterialApp оборачивает приложение в AnimatedTheme, который интерполирует ThemeData за 200 мс (kThemeAnimationDuration), поэтому всё, что прыгает, либо находится снаружи этого виджета, либо является значением, которое ThemeData.lerp не интерполирует.

    MaterialApp(  theme: AcmeTheme.light(),  darkTheme: AcmeTheme.dark(),  themeMode: mode,  // MaterialApp inserts AnimatedTheme; these are its knobs  themeAnimationDuration: const Duration(seconds: 1), // slow it down to debug…
  20. 20

    Как построить один пакет дизайн-системы для нескольких приложений и поменять токен, ничего в них не сломав?

    Пакет экспортирует семантические токены, фабрику ThemeData и брендовые компоненты (но не сырые константы), и эта поверхность версионируется как любой публичный API.

    // packages/acme_design/lib/acme_design.dart: the whole public surfaceexport 'src/theme/acme_theme.dart' show AcmeTheme;export 'src/theme/acme_tokens.dart' show AcmeTokens, AcmeSpacing, AcmeContextX;export 'src/components/acme_empty_state.dart' show AcmeEmptyState; // lib/src/theme/acme_theme.dart…
  21. 21

    Как не дать сырым цветам и отступам вне шкалы снова расползтись по коду, где дизайн-система уже есть?

    Сделайте это падением CI, а не комментарием на ревью: свой линт на сырые литералы вне пакета дизайн-системы, закоммиченный baseline, чтобы число нарушений могло только уменьшаться, и golden- и контрастные тесты по темам для того, чего линт не видит.

    double contrast(Color a, Color b) {  final la = a.computeLuminance();  final lb = b.computeLuminance();  final (hi, lo) = la > lb ? (la, lb) : (lb, la);  return (hi + 0.05) / (lo + 0.05);}…
  22. 22

    Вам досталось приложение с 400 захардкоженными цветами и стилями текста, а заморозить релизы нельзя. Как перевести его на дизайн-систему?

    Мигрируйте по паттерну strangler, фича за фичей, за слоем совместимости: сначала определите токены, затем направьте каждую легаси-константу на один из них, а дальше удаляйте константы экран за экраном, пока CI храповиком уменьшает остаток.

    // 1. inventory: grep -rEoh "Color\(0x[0-9A-Fa-f]{8}\)" lib | sort | uniq -c//    412 literals, 31 distinct values; 0xFFF5F5F5 and 0xFFF6F6F6 are one grey // 4. compat layer: nothing renders differently the day it lands,//    and the analyzer now lists every remaining call site@Deprecated('Use context.acme.surfaceMuted; removed in app 4.0')…