Flutter: Настройка тем приложения
Всем привет, читатели 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 и других стилей.
На этом у меня все. Надеюсь статья была полезной как и для новичков, так и опытные разработчики что-то из нее почерпнули. Оставляю ссылку на репозиторий, где можно увидеть данный вариант настройки тем.