Создание темы для Flutter-приложения

Учимся оформлять виджеты, создавать единые стили и динамически переключаться между различными темами оформления.

Уровень сложности: для начинающих

Скачать материалы

Версии программного обеспечения: Dart 2.10, Flutter 1.22, Android Studio 4.2

Одно из главных преимуществ Flutter заключается в том, что данный фреймворк предоставляет разработчику абсолютный контроль над поведением каждого пикселя. Это позволяет программисту воплотить в проекте любые дизайнерские идеи.

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

В этом руководстве мы рассмотрим, как применять стили к виджетам, создавать темы оформления и динамически переключаться между ними. Мы затронем следующие темы:

  1. Оформление отдельных виджетов.
  2. Применение общего стиля оформления к создаваемому приложению.
  3. Переключение между светлой и темной темой оформления.
  4. Приступим к оформлению нашего приложения.

Примечание: это руководство предполагает, что читатель знаком с основами работы в Flutter. Если это не так, начните с этого урока по созданию простого приложения на фреймворке, а также посмотрите урок по работе с виджетами интерфейса в Flutter.

Приступаем к работе

Загрузите файл стартового проекта, нажав на кнопку «Скачать материалы» в начале или в конце статьи. Затем откройте проект в Android Studio, после чего вы увидите файлы приложения Knight and Day («Рыцарь и день»). Это программа для учета рабочего времени рыцарей на дежурстве.

Запустите сборку и выполнение, и вы увидите главную страницу проекта:

На домашней странице Home вы увидите отчет о различных видах служебной деятельности рыцаря. Нажатие одной из трех доступных кнопок увеличит показатель выполнения соответствующего вида активности.

Хотя приложение уже работает, его оформление выглядит очень примитивным. Чтобы программа выгодно выделялась на фоне других подобных проектов, мы применим к виджетам кастомные стили оформления. Прежде, чем мы перейдем к написанию кода, давайте рассмотрим некоторые базовые сведения о стилях и темах оформления, а также о том, почему их применение действительно важно.

Темы оформления

Создание единой темы оформления заключается в использовании определенного набора цветов, шрифтов, форм и стилей во всех элементах и разделах приложения. Иными словами, это процесс создания уникального дизайна.

Поскольку большая часть виджетов оформляется в одинаковом стиле, не следует создавать различные стили для каждого виджета в отдельности. Вместо этого нужно определить стиль оформления в одном месте и потом применять его ко всем виджетам. Такой подход позволяет избежать появления повторяющихся фрагментов, что упрощает чтение, понимание и редактирование кода.

Если вам пока не очевидны преимущества единой темы оформления, не переживайте: все станет очевидно по мере того, как мы будем создавать тему оформления с нуля.

Использование тем в Flutter

Большинство видимых виджетов Flutter имеет свойство style; тип стиля варьируется в зависимости от вида виджета. В качестве примера можно привести TextStyle (виджет текстового поля Text) и ButtonStyle (кнопочный виджет Button). Изменения специфических стилей затронут внешний вид только тех виджетов, которым эти стили принадлежат.

Общепринятый подход к оформлению интерфейса в Flutter состоит в добавлении тематических Theme виджетов в общую иерархию. Библиотеки Material и Cupertino, кроме всего прочего, предоставляют встроенные темы для адаптации к языкам программирования, вместе с которыми они используются в различных приложениях.

Виджет Theme автоматически применяет свои стили ко всем вложенным элементам. Он принимает аргумент ThemeData, в котором содержатся определения для цветов и шрифтов. Если заглянуть в исходный код Flutter, можно заметить, что данный фреймворк использует InheritedWidge для распределения стилей среди виджетов в иерархии проекта.

Использовать темы оформления в Flutter достаточно просто. На следующем этапе мы подробно рассмотрим, как это делается.

Примечание: вложенные виджеты могут получить данные о стиле оформления из ThemeData, вызывая функцию Theme.of. Эта особенность может пригодиться, если потребуется создать вариацию на тему общего стиля с использованием метода copyWith для перезаписи атрибутов.

Создание стилей виджетов

Прежде всего, мы рассмотрим процесс задания специфического стиля для конкретного виджета, независимо от других виджетов. Откройте файл lib/home/home_page.dart и найдите метод build. Там расположены три виджета – простые объемные кнопки RaisedButton. Наша задача состоит в добавлении атрибутов формы и цвета в стиль оформления этих кнопок. Для этого замените метод build на следующий код:

@override
Widget build(BuildContext context) {
  final totalActivities = _joustCounter + _breakCounter + _patrolCounter;
  return Scaffold(
    appBar: CustomAppBar(
      title: 'Knight and Day',
    ),
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        BarChart(
          joustCounter: _joustCounter,
          breakCounter: _breakCounter,
          patrolCounter: _patrolCounter,
        ),
        const SizedBox(
          height: 32.0,
        ),
        Text('You\'ve done $totalActivities activities in total'),
        const SizedBox(
          height: 32.0,
        ),
        RaisedButton(
          child: const Text('Joust'),
          onPressed: () => setState(() { _joustCounter++; }),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
          color: CustomColors.lightPurple,
        ),
        RaisedButton(
          child: const Text('Take break'),
          onPressed: () => setState(() { _breakCounter++; }),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
          color: CustomColors.lightPurple,
        ),
        RaisedButton(
          child: const Text('Patrol'),
          onPressed: () => setState(() { _patrolCounter++; }),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
          color: CustomColors.lightPurple,
        ),
      ],
    ),
  );
}

Теперь у всех кнопок RaisedButton есть новый атрибут формы, который превратил их в скругленные прямоугольники, и новый цвет – светло-пурпурный. Запустите сборку и выполнение, посмотрите на стильные новые кнопки.

Примечание: скорее всего, вы заметили, что в приведенном выше коде используется класс CustomColors из файла lib/theme/colors.dart. В соответствии с принципом DRY («не повторяйся»), этот класс предназначен для хранения статических значений для различных цветов, которые мы будем использовать в ходе этого руководства. Если вам захочется изменить какой-то цвет, достаточно будет внести поправки в CustomColors, вместо того, чтобы искать и заменять значение в коде всего приложения.

Создание единой темы оформления

Как вы уже убедились, изменить стиль одного-двух виджетов очень просто. Однако если вам захочется изменить стиль множества виджетов по отдельности, работа станет слишком утомительной. В идеале, единый стиль оформления должен создаваться и храниться в одном месте, чтобы определять внешний вид сразу всех элементов приложения. К счастью, это возможно с помощью подключения темы Theme к библиотеке MaterialApp.

Для этого нужно создать новый файл custom_theme.dart в директории lib/theme и вставить в него приведенный ниже код:

import 'package:flutter/material.dart';

import 'colors.dart';

class CustomTheme {
  static ThemeData get lightTheme { //1
    return ThemeData( //2
      primaryColor: CustomColors.purple,
      scaffoldBackgroundColor: Colors.white,
      fontFamily: 'Montserrat', //3
      buttonTheme: ButtonThemeData( // 4
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
        buttonColor: CustomColors.lightPurple,
      )
    );
  }
}

Вам, скорее всего, не составило труда разобраться, что именно делает этот код:

  1. Создает глобальную функцию для переключения темы оформления – мы воспользуемся ей позже.
  2. Конструирует пользовательский набор атрибутов ThemeData. Обратите внимание на число переопределяемых атрибутов – пока что мы используем всего несколько опций из множества возможных.
  3. Определяет семейство шрифтов, которые будут по умолчанию отображаться в приложении.
  4. Задает стиль для кнопок, похожий на тот, что мы ранее использовали в файле lib/home/home_page.dart.

Осталось лишь применить созданную тему оформления к приложению. Откройте файл lib/main.dart и замените его содержимое на следующий код:

import 'package:flutter/material.dart';
import 'package:knight_and_day/home/home_page.dart';
import 'package:knight_and_day/theme/custom_theme.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Knight and Day',
      home: HomePage(),
      theme: CustomTheme.lightTheme,
    );
  }
}

Единственное, что делает этот код – использует атрибуты темы оформления из MaterialApp и передает настройки пользовательской темы, которую мы создали на предыдущем этапе. Запустите сборку и выполнение – и вы заметите, что тип шрифта изменился глобально, во всех разделах приложения.

Теперь, когда вы установили единую тему оформления, включающую в себя цвета, шрифты и стили кнопок, пора удалить экспериментальный стиль, который мы ранее использовали для RaisedButtons. Для этого верните метод build в файле lib/home/home_page.dart в первоначальный вид:

@override
Widget build(BuildContext context) {
  final totalActivities = _joustCounter + _breakCounter + _patrolCounter;
  return Scaffold(
    appBar: CustomAppBar(
      title: 'Knight and Day',
    ),
    body: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        BarChart(
          joustCounter: _joustCounter,
          breakCounter: _breakCounter,
          patrolCounter: _patrolCounter,
        ),
        const SizedBox(
          height: 32.0,
        ),
        Text('You\'ve done $totalActivities activities in total'),
        const SizedBox(
          height: 32.0,
        ),
        RaisedButton(
          child: const Text('Joust'),
          onPressed: () => setState(() { _joustCounter++; }),
        ),
        RaisedButton(
          child: const Text('Take break'),
          onPressed: () => setState(() { _breakCounter++; }),
        ),
        RaisedButton(
          child: const Text('Patrol'),
          onPressed: () => setState(() { _patrolCounter++; }),
        ),
      ],
    ),
  );
}

Запустите сборку и исполнение кода, и вы увидите, что кнопки сохранили свой стиль, несмотря на удаление атрибутов цвета и формы из каждой RaisedButton. Стиль оформления кнопки теперь получают из темы, которую мы добавили в MaterialApp. Отличная работа! Основа для единой темы оформления приложения успешно создана.

Оформление текстовых виджетов

Текстовые виджеты – особый случай: для их оформления ThemeData предоставляет множество стилей на выбор. Для практической демонстрации измените стиль виджета с текстом «You’ve done x activities in total» («Всего вы совершили Х активностей») в файле lib/home/home_page.dart на следующий:

Text(
  'You\'ve done $totalActivities activities in total',
  style: Theme.of(context).textTheme.headline6,
),

Запустите сборку, исполнение кода и вы увидите, как изменился стиль надписи:

Не бойтесь поэкспериментировать с различными стилями объекта TextTheme. Вы найдете там стили, разделенные по подгруппам для заголовков, подписей и основного текста. Манипулировать стилями очень легко, а результат при этом достигается действительно впечатляющий.

Создание темной темы оформления

В наши дни практически все операционные системы и приложения позволяют использовать темную тему оформления – из дизайнерских соображений, или чтобы облегчить чтение и просмотр при слабом освещении. Пользователям нашего приложения также необходима подобная опция.

Для создания темной темы оформления, откройте файл lib/theme/custom_theme.dart и добавьте приведенный ниже код сразу под lightTheme:

static ThemeData get darkTheme {
  return ThemeData(
    primaryColor: CustomColors.darkGrey,
    scaffoldBackgroundColor: Colors.black,
    fontFamily: 'Montserrat',
    textTheme: ThemeData.dark().textTheme,
    buttonTheme: ButtonThemeData(
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18.0)),
      buttonColor: CustomColors.lightPurple,
    )
  );
}

Этот фрагмент кода, на первый взгляд, может показаться достаточно сложным. На самом деле он использует ту же структуру, что и светлая тема оформления lightTheme. Разница состоит лишь в том, что цвета и стили в этом блоке формируют темный дизайн.

Чтобы увидеть код в действии, откройте файл lib/main.dart и замените атрибуты темы на параметры нашего темного стиля:

return MaterialApp(
  title: 'Knight and Day',
  home: HomePage(),
  theme: CustomTheme.darkTheme,
);

Здесь мы просто передаем атрибуты CustomTheme.darkTheme вместо параметров светлой темы CustomTheme.lightTheme. Запустите сборку, исполнение, и оцените новый стильный дизайн:

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

Переключение между темами

Чтобы переключаться между темами оформления нажатием одной кнопки, необходимо отслеживать состояние текущей темы оформления на глобальном уровне. Для этого мы создадим файл конфигурации config.dart в папке lib/theme и добавим в него следующий код:

import 'package:knight_and_day/theme/custom_theme.dart';
CustomTheme currentTheme = CustomTheme();

Здесь мы устанавливаем использование кастомной темы оформления для всего приложения. Затем откройте файл lib/theme/custom_theme.dart и добавьте следующий код в начале CustomTheme:

static bool _isDarkTheme = true;
ThemeMode get currentTheme => _isDarkTheme ? ThemeMode.dark : ThemeMode.light;

void toggleTheme() {
  _isDarkTheme = !_isDarkTheme;
  notifyListeners();
}

Данный код выполняет две функции. Во-первых, объявляется переменная, которая отслеживает, является ли активная тема оформления темной и передает значение _isDarkTheme в соответствующий параметр ThemeMode.

Во-вторых, мы создали метод, который вызывает _isDarkTheme и уведомляет об этом все элементы интерфейса. Для дальнейшей реализации уведомления об изменении стиля оформления необходимо создать класс ChangeNotifier. Для этого мы изменим подпись CustomTheme на следующий код:

class CustomTheme with ChangeNotifier {
...
}

Теперь откройте файл lib/main.dart и замените его содержимое на приведенный ниже код. Не забудьте импортировать конфигурацию lib/theme/config.dart:

import 'package:flutter/material.dart';
import 'package:knight_and_day/home/home_page.dart';
import 'package:knight_and_day/theme/config.dart';
import 'package:knight_and_day/theme/custom_theme.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key key}): super(key: key);
  //1
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State {
  @override
  void initState() {
    super.initState();
    currentTheme.addListener(() {
      //2
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Knight and Day',
      home: HomePage(),
      theme: CustomTheme.lightTheme, //3
      darkTheme: CustomTheme.darkTheme, //4
      themeMode: currentTheme.currentTheme, //5
    );
  }
}

Рассмотрим, что именно делает этот код:

  1. Изменяет тип MyApp с виджета без состояния на виджет с сохранением состояния.
  2. Для получения уведомлений от currentTheme с помощью ChangeNotifier об изменении темы оформления, создается подписка на состояние initState. Для обновления виджета вызывается метод setState.
  3. Устанавливает светлую тему оформления по умолчанию.
  4. Устанавливает темную тему.
  5. Благодаря этому атрибуту MaterialApp узнает, какую тему оформления необходимо применить к интерфейсу – светлую или темную.

Последний шаг на данном этапе – создание кнопки, которая будет менять тему оформления одним нажатием. Откройте файл lib/custom_app_bar.dart и замените IconButton на приведенный ниже код:

IconButton(
  icon: const Icon(Icons.brightness_4),
  onPressed: () => currentTheme.toggleTheme(),
)

Здесь мы добавили событие onPressed, которое будет вызывать переключение между темами оформления. Незабудьте импортировать lib/theme/config.dart:

import 'package:knight_and_day/theme/config.dart';

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

Что делать дальше

Вы можете скачать финальный вариант проекта, нажав на кнопку «Скачать материалы» в начале или в конце статьи.

Примите поздравления с успешным созданием первой темы оформления. Теперь вы знакомы с процессом создания тем оформления и переключения между ними, и можете вывести свои Flutter-приложения на новый уровень. Стильный дизайн и удобный интерфейс – ключевые факторы привлечения пользователей.

Если у вас есть желание продолжить работу с Flutter, обратите внимание на статью «Ваше первое приложение на Flutter». Также рекомендую прочитать эту книгу по работе с Flutter. Предложения, достижения по разработке тем и вопросы оставляйте в комментариях.

Данная публикация является переводом статьи «Theming a Flutter App: Getting Started» , подготовленная редакцией проекта.

Меню