🎨 Flutter Theme: управляйте как сеньор
FlutterPulseЭта статья переведена специально для канала FlutterPulse. В этом канале вы найдёте много интересных вещей, связанных с Flutter. Не забывайте подписываться! 🚀

Flutter Theme: управляйте как сеньор
Из названия вы могли подумать "Ууууххх", так как темизация во Flutter может быть головной болью для управления и реализации с множеством классов. Я хочу показать вам, как делать это последовательно, с учётом принципа единственной ответственности и разделения ответственности.
Подготовка
Сначала давайте взглянем на дизайн приложения, включая такие функции, как цвета и шрифты, которые мы будем использовать. У нас есть светлая тема, тёмная тема и цветовая палитра.
Реализация
Начнём с нового проекта. Внутри папки lib создадим базовую структуру папок:
lib
├── presentation
│ └── style
│ ├── colors.dart
│ ├── text_styles.dart
│ ├── extensions.dart
│ └── app_theme.dart
└── main.dartЦвета
// СВЕТЛАЯ ЦВЕТОВАЯ ПАЛИТРА
const Color backgroundColorLight = Color(0xFFF2F2F2);
const Color primaryColorLight = Color(0xFF1E3C77);
const Color secondaryColorLight = Color(0xFFFFCC00);
const Color textColorLight = Color(0xFF212526);
const Color errorColorLight = Color(0xFFC02942);
// ТЁМНАЯ ЦВЕТОВАЯ ПАЛИТРА
const Color backgroundColorDark = Color(0xFFF2F2F2);
const Color primaryColorDark = Color(0xFF1E3C77);
const Color secondaryColorDark = Color(0xFFFFCC00);
const Color textColorDark = Color(0xFF212526);
const Color errorColorDark = Color(0xFFC02942);Я просто определил все цвета и разделил их на светлую и тёмную палитры. Таким образом у нас есть один файл, который содержит все цвета, используемые в приложении.
Стили текста
// ОБЩИЕ
const TextStyle titleTextStyle = TextStyle(fontSize: 28, fontWeight: FontWeight.bold, fontFamily: 'Roboto Condensed');
const TextStyle subtitleTextStyle = TextStyle(fontSize: 16, fontFamily: 'SF Pro Rounded');
const TextStyle labelTextStyle = TextStyle(fontSize: 12, fontFamily: 'SF Pro Rounded');
// БЛОГИ
const TextStyle trendingBlogTitleTextStyle = TextStyle(fontSize: 22, fontWeight: FontWeight.bold, fontFamily: 'Roboto Condensed');
const TextStyle cardBlogTitleTextStyle = TextStyle(fontSize: 14, fontWeight: FontWeight.bold, fontFamily: 'Roboto Condensed');Я разделил стили текста здесь по темам, в то время как те, которые используются во всём приложении, определены вверху.
AppTheme с расширениями
Цвет ошибки не существует как часть класса ThemeData, но мы можем сделать его частью ThemeData, используя расширения:
class AppColors extends ThemeExtension<AppColors> {
final Color? backgroundColor;
final Color? primaryColor;
final Color? secondaryColor;
final Color? textColor;
final Color? errorColor;
const AppColors({
required this.backgroundColor,
required this.primaryColor,
required this.secondaryColor,
required this.textColor,
required this.errorColor,
});
@override
ThemeExtension<AppColors> copyWith({...}) => AppColors(...);
@override
ThemeExtension<AppColors> lerp(covariant ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) return this;
return AppColors(
backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t),
primaryColor: Color.lerp(primaryColor, other.primaryColor, t),
secondaryColor: Color.lerp(secondaryColor, other.secondaryColor, t),
textColor: Color.lerp(textColor, other.textColor, t),
errorColor: Color.lerp(errorColor, other.errorColor, t),
);
}
}Настройка AppTheme
class AppTheme {
static final lightTheme = ThemeData(
fontFamily: 'SF Pro Rounded',
shadowColor: Colors.black26,
extensions: [
AppColors(
backgroundColor: backgroundColorLight,
primaryColor: primaryColorLight,
secondaryColor: secondaryColorLight,
textColor: textColorLight,
errorColor: errorColorLight,
),
],
);
static final darkTheme = ThemeData(
fontFamily: 'SF Pro Rounded',
shadowColor: Colors.black26,
extensions: [
AppColors(
backgroundColor: backgroundColorDark,
primaryColor: primaryColorDark,
secondaryColor: secondaryColorDark,
textColor: textColorDark,
errorColor: errorColorDark,
),
],
);
}Расширение для кастомных стилей текста
extension CustomTextStyles on TextTheme {
TextStyle get titleStyle => titleTextStyle;
TextStyle get subtitleStyle => subtitleTextStyle;
TextStyle get labelStyle => labelTextStyle;
TextStyle get trendingBlogTitleStyle => trendingBlogTitleTextStyle;
TextStyle get cardBlogTitleStyle => cardBlogTitleTextStyle;
}Расширения BuildContext для чистого доступа
extension StyleExtension on BuildContext {
Color get colorBackground => Theme.of(this).extension<AppColors>()!.backgroundColor!;
Color get colorPrimary => Theme.of(this).extension<AppColors>()!.primaryColor!;
Color get colorSecondary => Theme.of(this).extension<AppColors>()!.secondaryColor!;
Color get colorError => Theme.of(this).extension<AppColors>()!.errorColor!;
Color get colorText => Theme.of(this).extension<AppColors>()!.textColor!;
TextStyle get textTitle => Theme.of(this).textTheme.titleStyle;
TextStyle get textSubtitle => Theme.of(this).textTheme.subtitleStyle;
TextStyle get textLabel => Theme.of(this).textTheme.labelStyle;
}Итоговое использование
class HomeWidget extends StatelessWidget {
const HomeWidget({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: context.colorBackground,
body: Center(
child: Text(
'Home',
style: context.textTitle,
),
),
);
}
}Теперь, когда мы вызываем свойства темы, код становится намного чище, а с префиксами вроде "color" или "text" вы получаете отфильтрованные доступные опции в выпадающем меню.
Заключение
После многих проектов экспериментов этот подход показал лучшие результаты. Он помогает вам управлять темизацией более последовательно и облегчает поддержку и изменения. Ключ в использовании расширений на BuildContext для обеспечения чистого, интуитивного доступа к свойствам вашей темы.
Удачного кодинга! 🎨