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; } 

В конце статьи хочу обратить ваше внимание на следующие вещи:

  1. В Theme Extensions давайте названия, которые относятся к определенному виджету. Например, у вас есть виджет Content и текстовые стили для него лучше всего называть contentStatus, contentTitle…

  2. Не поленитесь создать несколько разных стилей. Допустим, у вас в Row находиться две кнопки с одинаковым цветом. Вы можете создать один цвет в расширениях, однако если потом дизайнер поменяет цвет первой кнопки и вы его изменете в коде, то сразу поменяется цвет другой. И вам все равно придется создавать отдельное свойство.

  3. Пробуйте экспериментировать! Вы можете создать также extension для ButtonStyles и других стилей.

На этом у меня все. Надеюсь статья была полезной как и для новичков, так и опытные разработчики что-то из нее почерпнули. Оставляю ссылку на репозиторий, где можно увидеть данный вариант настройки тем.


ссылка на оригинал статьи https://habr.com/ru/post/690572/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *