Всем привет, читатели Habr! В начале я хочу сделать акцент на том, что статья ориентирована для новичков, однако может быть полезной для более опытных коллег. В этой статье я расскажу про то, что такое тема приложения, какие ошибки обычно делают новички и рассмотрю, как по мне, элегантный вариант настройки тем.
Что такое тема приложения
Тема приложения — коллекция всех стилей приложения, которая обеспечивает ему профессиональный вид. Это все цвета ваших виджетов, все градиенты, а также стили текста, кнопок и т.д. Если проводить аналогию с человеком, то это вся одежда на нем.
Какие ошибки допускают новички, стилизируя приложение
Я считаю, что большинство новичков нарушают принцип DRY(Don’t repeat yourself). Очень часто в коде можно увидеть подобную картину.

Как видно из картинки выше, текстовые стили одинаковы и занимают суммарно 10 строк кода. Представьте, если в колонке не два текстовых поля, а десять :)
Самым простым решением этой проблемы будет создать абстрактный класс AppTextStyles со статическим полем и потом писать style: AppTextStyles.yourStyle.
У такого решения есть недостаток — что если у нас в приложении больше, чем одна тема? Скорее всего придется создать классы AppDarkTextStyles/AppLightTextStyles и потом, задавая стиль текстовому виджету, проверять, какая сейчас текущая тема и в зависимости от этого выбирать нужную константу. По сути, при каждом указании стиля нужно будет писать тернарный оператор. Не думаю, что это выглядит хорошо.
Во Flutter есть красивое решение этой проблемы — использовать встроенный механизм тем. В документации написано, что темы нужны для того, чтобы делиться стилями по всему приложению. В MaterialApp вы можете задать тему через свойство theme. Для этого вам нужно создать объект типа ThemeData, где вы укажете все необходимые стили и цвета. Например, вы можете указать все необходимые текстовые стили в свойстве textTheme.
После этого, можно обратиться к нужному стилю через Theme.of(context).textTheme. В таком варианте проблема с тем, что нужно использовать тернарный оператор в зависимости от темы, решена.
Теперь возникает другая сложность. Допустим вы задали все 27 свойств TextTheme и вам все равно мало:) У вас есть вариант создать новый стиль, используя метод copyWith() при добавлении стиля к виджету, однако это будет засорять build метод. Или вам банально не нравиться названия свойств TextTheme. Во Flutter на этот счет тоже есть решение - Theme Extensions!
Элегантный вариант настройки тем
Theme Extensions — это произвольное дополнение к теме. Благодаря этому механизму мы можем создать дополнения к текстовой теме, к цветам приложения или прописать все градиенты, которые используются в приложении. Предлагаю рассмотреть мою структуру папки с темой.

Файл theme.dart представляет собой файл, в котором собраны все зависимости.
import 'package:flutter/material.dart'; part 'src/constants.dart'; part 'src/dark_theme.dart'; part 'src/light_theme.dart'; part 'src/text_theme.dart'; part 'src/theme_colors.dart'; part 'src/theme_text_styles.dart'; part 'src/theme_gradients.dart';
В файлах light_theme.dart и dark_theme.dart создаются светлая и темная темы. Для примера покажу светлую тему.
part of '../theme.dart'; ThemeData createLightTheme() { return ThemeData( textTheme: createTextTheme(), brightness: Brightness.light, scaffoldBackgroundColor: AppColors.white, extensions: <ThemeExtension<dynamic>>[ ThemeColors.light, ThemeTextStyles.light, ThemeGradients.light, ], dialogTheme: DialogTheme( backgroundColor: AppColors.white, titleTextStyle: headline1.copyWith( color: AppColors.black, fontSize: 20, fontWeight: FontWeight.w500, ), contentTextStyle: headline1.copyWith( color: AppColors.black, ), ), focusColor: Colors.blue.withOpacity(0.2), appBarTheme: AppBarTheme(backgroundColor: Colors.white), ); }
В файле text_theme.dart создается текстовая тема, хотя в самом приложении я ни разу не обращаюсь к headline1 или другим свойствам.
part of '../theme.dart'; TextTheme createTextTheme() { return const TextTheme( headline1: headline1, headline2: headline2, ); }
constants.dart содержит в себе константные текстовые стили, которые я потом использую в theme_text_styles.dart, и класс AppColors, в котором прописаны базовые цвета через класс Colors, а также разные оттенки. В этом месте может возникнуть вопрос: зачем прописывать базовые цвета? Я это делаю для того, чтобы в дальнейшем избежать пересечения Colors и AppColors. Это дело вкуса :)
part of '../theme.dart'; const headline1 = TextStyle(fontWeight: FontWeight.w400, fontSize: 16); const headline2 = TextStyle(fontWeight: FontWeight.w400, fontSize: 14); abstract class AppColors { static const white = Colors.white; static const black = Colors.black; static const blue = Colors.blue; static const red = Colors.red; static const darkerRed = Color(0xFFCB5A5E); static const grey = Colors.grey; static const darkerGrey = Color(0xFF6C6C6C); static const darkestGrey = Color(0xFF626262); static const lighterGrey = Color(0xFF959595); static const lightGrey = Color(0xFF5d5d5d); static const lighterDark = Color(0xFF272727); static const lightDark = Color(0xFF1b1b1b); static const purpleAccent = Colors.purpleAccent; }
Перейдем к самому интересному моменту — Theme Extensions. Допустим, у меня есть кнопка фильтра и ее цвет отличается от основных цветов ThemeData. Здесь на помощь приходят расширения. С помощью них мы можем создать Color filterButtonFillColor и получить его через BuildContext. В этом же классе мы можем прописать какой цвет будет использоваться в светлой и темной темах.
part of '../theme.dart'; class ThemeColors extends ThemeExtension<ThemeColors> { final Color filterButtonFillColor; const ThemeColors({ required this.filterButtonFillColor, }); @override ThemeExtension<ThemeColors> copyWith({ Color? filterButtonFillColor, }) { return ThemeColors( filterButtonFillColor: filterButtonFillColor ?? this.filterButtonFillColor, ); } @override ThemeExtension<ThemeColors> lerp( ThemeExtension<ThemeColors>? other, double t, ) { if (other is! ThemeColors) { return this; } return ThemeColors( filterButtonFillColor: Color.lerp(filterButtonFillColor, other.filterButtonFillColor, t)!, ); } static get light => ThemeColors( filterButtonFillColor: AppColors.grey, ); static get dark => ThemeColors( filterButtonFillColor: AppColors.white, ); }
Последним шагом стоит добавить расширения для светлой и темных тем.
return ThemeData( textTheme: createTextTheme(), brightness: Brightness.light, scaffoldBackgroundColor: AppColors.white, extensions: <ThemeExtension<dynamic>>[ ThemeColors.light, ThemeTextStyles.light, ThemeGradients.light, ], );
Я написал расширения для цветов, для текстовых стилей и градиентов. После этого мы можем получить необходимый нам цвет через Theme.of(context).extension<ThemeColors>!.neededColor. Однако, чтобы не писать такую конструкцию каждый раз, можно создать расширения над BuildContext и после этого писать context.color или context.text и т.д.
import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:meta_app/presentation/themes/theme.dart'; extension BuildContextExt on BuildContext { AppLocalizations get localizations => AppLocalizations.of(this)!; ThemeTextStyles get text => Theme.of(this).extension<ThemeTextStyles>()!; ThemeColors get color => Theme.of(this).extension<ThemeColors>()!; ThemeGradients get gradient => Theme.of(this).extension<ThemeGradients>()!; bool get isDarkMode => Theme.of(this).brightness == Brightness.dark; }
В конце статьи хочу обратить ваше внимание на следующие вещи:
В Theme Extensions давайте названия, которые относятся к определенному виджету. Например, у вас есть виджет Content и текстовые стили для него лучше всего называть contentStatus, contentTitle…
Не поленитесь создать несколько разных стилей. Допустим, у вас в Row находиться две кнопки с одинаковым цветом. Вы можете создать один цвет в расширениях, однако если потом дизайнер поменяет цвет первой кнопки и вы его изменете в коде, то сразу поменяется цвет другой. И вам все равно придется создавать отдельное свойство.
Пробуйте экспериментировать! Вы можете создать также extension для ButtonStyles и других стилей.
На этом у меня все. Надеюсь статья была полезной как и для новичков, так и опытные разработчики что-то из нее почерпнули. Оставляю ссылку на репозиторий, где можно увидеть данный вариант настройки тем.
