1. Введение
Flutter – это набор инструментов пользовательского интерфейса от Google, который позволяет разрабатывать приложения для мобильных устройств, браузеров и ПК, используя единую базу кода. В этой практической работе вы создадите следующее приложение Flutter:
Приложение генерирует звучные названия, например "newstay", "lightstream", "mainbrake" или "graypine". Пользователь может попросить сгенерировать следующее слово, добавить текущее в избранное и посмотреть список избранных слов на отдельной странице. Приложение адаптируется к разным размерам экрана.
Что вы узнаете
- Основы работы с Flutter
- Создание макетов во Flutter
- Связывание действий пользователя (например, нажатий кнопок) с поведением приложения.
- Как упорядочить код Flutter
- Как сделать приложение адаптивным (для разных экранов)
- Как добиться единообразного внешнего вида приложения
Вы начнете с базового шаблона, чтобы сразу перейти к интересным частям.

А вот Филип, который проведет вас через всю практическую работу!
Чтобы начать лабораторную работу, нажмите "Далее".
2. Как настроить среду Flutter
Редактор
Чтобы упростить эту практическую работу, мы предполагаем, что вы будете использовать Visual Studio Code (VS Code) в качестве среды разработки. Это бесплатный сервис, который работает на всех основных платформах.
Конечно, вы можете использовать любой редактор, который вам нравится: Android Studio, другие IDE IntelliJ, Emacs, Vim или Notepad++. Все они работают с Flutter.
Мы рекомендуем использовать VS Code для этой практической работы, поскольку в инструкциях по умолчанию используются сочетания клавиш, характерные для VS Code. Проще сказать "нажмите здесь" или "нажмите эту клавишу", чем "выполните соответствующее действие в редакторе, чтобы сделать X".

Выберите цель разработки
Flutter – это мультиплатформенный набор инструментов. Приложение может работать в любой из следующих операционных систем:
- iOS
- Android
- Windows
- macOS
- Linux
- веб
Однако на практике обычно выбирают одну операционную систему, для которой в основном разрабатывают приложения. Это ваша "целевая платформа" – операционная система, на которой ваше приложение работает во время разработки.

Например, вы разрабатываете приложение Flutter на ноутбуке с Windows. Если вы выберете Android в качестве целевой платформы, то обычно вы подключаете устройство Android к ноутбуку с Windows с помощью USB-кабеля, и разрабатываемое приложение запускается на этом подключенном устройстве Android. Но вы также можете выбрать Windows в качестве целевой платформы, и тогда разрабатываемое приложение будет запускаться как приложение Windows вместе с редактором.
У вас может возникнуть соблазн выбрать веб-платформу в качестве цели разработки. Недостаток этого подхода в том, что вы теряете одну из самых полезных функций разработки Flutter: Stateful Hot Reload. Flutter не поддерживает горячую перезагрузку веб-приложений.
Сделайте выбор. Помните, что вы всегда можете запустить приложение на других операционных системах позже. Просто наличие четкой цели разработки облегчает следующий шаг.
Установите Flutter
Самые актуальные инструкции по установке Flutter SDK всегда доступны на сайте docs.flutter.dev.
Инструкции на сайте Flutter охватывают не только установку самого SDK, но и инструменты, связанные с целевой платформой разработки, и плагины редактора. Для этой практической работы вам нужно установить только следующее:
- Flutter SDK
- Visual Studio Code с плагином Flutter;
- Программное обеспечение, необходимое для выбранной целевой платформы (например, Visual Studio для Windows или Xcode для macOS).
В следующем разделе вы создадите свой первый проект Flutter.
Если у вас возникли проблемы, вы можете найти некоторые из этих вопросов и ответов (из StackOverflow), которые помогут устранить неполадки.
Часто задаваемые вопросы
- Как найти путь к Flutter SDK?
- Что делать, если команда Flutter не найдена?
- Как устранить проблему "Waiting for another flutter command to release the startup lock" (Ожидание другой команды flutter для снятия блокировки запуска)?
- Как указать Flutter, где установлен Android SDK?
- Как устранить ошибку Java при запуске
flutter doctor --android-licenses? - Что делать, если инструмент Android
sdkmanagerне найден? - Как устранить ошибку "Отсутствует компонент
cmdline-tools"? - Как запустить CocoaPods на Apple Silicon (M1)?
- Как отключить автоматическое форматирование при сохранении в VS Code?
3. Как создать проект
Создайте свой первый проект Flutter
Запустите Visual Studio Code и откройте панель команд (с помощью F1, Ctrl+Shift+P или Shift+Cmd+P). Начните вводить "flutter new". Выберите команду Flutter: New Project.
Затем выберите Application (Приложение) и папку, в которой нужно создать проект. Это может быть ваш домашний каталог или, например, C:\src\.
Назовите проект. Например, namer_app или my_awesome_namer.

Flutter создаст папку проекта, и VS Code откроет ее.
Теперь вы перезапишете содержимое трех файлов базовым каркасом приложения.
Скопируйте и вставьте исходное приложение
На левой панели VS Code выберите Проводник и откройте файл pubspec.yaml.

Замените содержимое этого файла следующим:
pubspec.yaml
name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
Файл pubspec.yaml содержит основную информацию о приложении, например его текущую версию, зависимости и объекты, с которыми оно будет поставляться.
Затем откройте другой файл конфигурации в проекте – analysis_options.yaml.

Замените его содержимое следующим:
analysis_options.yaml
include: package:flutter_lints/flutter.yaml
linter:
rules:
avoid_print: false
prefer_const_constructors_in_immutables: false
prefer_const_constructors: false
prefer_const_literals_to_create_immutables: false
prefer_final_fields: false
unnecessary_breaks: true
use_key_in_widget_constructors: false
Этот файл определяет, насколько строгим должен быть Flutter при анализе вашего кода. Поскольку это ваше первое приложение Flutter, вы указываете анализатору не быть слишком строгим. Вы всегда сможете изменить эти настройки позже. На самом деле, по мере приближения к публикации реального рабочего приложения вы, скорее всего, захотите сделать анализатор более строгим.
Откройте файл main.dart в каталоге lib/.

Замените содержимое этого файла следующим:
lib/main.dart
import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
return Scaffold(
body: Column(
children: [Text('A random idea:'), Text(appState.current.asLowerCase)],
),
);
}
}
Эти 50 строк кода – все, что пока есть в приложении.
В следующем разделе запустите приложение в режиме отладки и начните разработку.
4. Добавить кнопку
На этом шаге мы добавим кнопку Next (Далее), чтобы генерировать новые пары слов.
Запустите приложение.
Сначала откройте lib/main.dart и убедитесь, что выбрано целевое устройство. В правом нижнем углу VS Code вы увидите кнопку, на которой указано текущее целевое устройство. Нажмите, чтобы изменить его.
Открыв файл lib/main.dart, найдите кнопку "Воспроизвести"
в правом верхнем углу окна VS Code и нажмите ее.
Примерно через минуту приложение запустится в режиме отладки. Пока он выглядит не очень впечатляюще:

Первая горячая перезагрузка
В нижней части файла lib/main.dart добавьте что-нибудь в строку в первом объекте Text и сохраните файл (с помощью сочетания клавиш Ctrl+S или Cmd+S). Например:
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'), // ← Example change.
Text(appState.current.asLowerCase),
],
),
);
// ...
Обратите внимание, что приложение меняется сразу, а случайное слово остается прежним. Это знаменитая функция горячей перезагрузки с сохранением состояния Flutter. Горячая перезагрузка запускается, когда вы сохраняете изменения в исходном файле.
Часто задаваемые вопросы
- Что делать, если горячая перезагрузка не работает в VSCode?
- Нужно ли нажимать клавишу R для горячей перезагрузки в VS Code?
- Работает ли горячая перезагрузка в веб-приложениях?
- Как убрать баннер "Отладка"?
Добавление кнопки
Затем добавьте кнопку внизу Column, прямо под вторым экземпляром Text.
lib/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(appState.current.asLowerCase),
// ↓ Add this.
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
],
),
);
// ...
После сохранения изменений приложение снова обновится: появится кнопка, при нажатии на которую в консоли отладки VS Code появится сообщение button pressed!.
Краткий курс по Flutter за 5 минут
Наблюдать за консолью отладки интересно, но кнопка должна выполнять более полезное действие. Но сначала давайте посмотрим на код в файле lib/main.dart, чтобы понять, как он работает.
lib/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
В самом верху файла вы найдете функцию main(). В текущем виде он только указывает Flutter запустить приложение, определенное в MyApp.
lib/main.dart
// ...
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
Класс MyApp расширяет класс StatelessWidget. Виджеты – это элементы, из которых вы создаете каждое приложение Flutter. Как видите, даже само приложение является виджетом.
Код в MyApp настраивает все приложение. Он создает состояние на уровне приложения (подробнее об этом позже), называет приложение, определяет визуальную тему и устанавливает виджет "home" – начальную точку вашего приложения.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
Далее класс MyAppState определяет состояние приложения. Это ваше первое знакомство с Flutter, поэтому практическая работа будет простой и узконаправленной. В Flutter есть много эффективных способов управления состоянием приложения. Один из самых простых – ChangeNotifier, который используется в этом приложении.
MyAppStateопределяет, какие данные нужны приложению для работы. Сейчас в нем только одна переменная с текущей случайной парой слов. Вы добавите его позже.- Класс состояния расширяет
ChangeNotifier, что позволяет ему уведомлять другие классы о своих изменениях. Например, если текущая пара слов меняется, некоторые виджеты в приложении должны знать об этом. - Состояние создается и предоставляется всему приложению с помощью
ChangeNotifierProvider(см. код выше вMyApp). Это позволяет любому виджету в приложении получить состояние.

lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) { // ← 1
var appState = context.watch<MyAppState>(); // ← 2
return Scaffold( // ← 3
body: Column( // ← 4
children: [
Text('A random AWESOME idea:'), // ← 5
Text(appState.current.asLowerCase), // ← 6
ElevatedButton(
onPressed: () {
print('button pressed!');
},
child: Text('Next'),
),
], // ← 7
),
);
}
}
// ...
Наконец, есть виджет MyHomePage, который вы уже изменили. Каждая пронумерованная строка ниже соответствует комментарию с номером строки в коде выше:
- В каждом виджете определен метод
build(), который автоматически вызывается при каждом изменении условий виджета, чтобы он всегда был актуальным. MyHomePageотслеживает изменения текущего состояния приложения с помощью методаwatch.- Каждый метод
buildдолжен возвращать виджет или (что более типично) вложенное дерево виджетов. В этом случае виджет верхнего уровня –Scaffold. В этой практической работе вы не будете использоватьScaffold, но это полезный виджет, который встречается в большинстве реальных приложений Flutter. Column– один из самых простых виджетов макета во Flutter. Он принимает любое количество дочерних элементов и размещает их в столбце сверху вниз. По умолчанию столбец визуально размещает свои дочерние элементы вверху. Скоро вы измените это, чтобы столбец был по центру.- Вы изменили этот виджет (
Text) на первом шаге. - Второй виджет
TextпринимаетappStateи обращается к единственному элементу этого класса –current(который являетсяWordPair).WordPairпредоставляет несколько полезных геттеров, таких какasPascalCaseилиasSnakeCase. Здесь мы используемasLowerCase, но вы можете выбрать другой вариант. - Обратите внимание, что в коде Flutter часто используются конечные запятые. В данном случае запятая не нужна, поскольку
children– последний (и единственный) элемент в списке параметровColumn. Тем не менее, обычно рекомендуется использовать завершающие запятые: они упрощают добавление дополнительных элементов, а также служат подсказкой для автоформатировщика Dart, чтобы он вставил там новую строку. Подробнее о форматировании кода…
Теперь подключите кнопку к состоянию.
Первое поведение
Прокрутите страницу до раздела MyAppState и добавьте способ getNext.
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
// ↓ Add this.
void getNext() {
current = WordPair.random();
notifyListeners();
}
}
// ...
Новый метод getNext() переназначает current с новым случайным WordPair. Он также вызывает notifyListeners() (метод ChangeNotifier), который гарантирует, что все, кто наблюдает за MyAppState, получат уведомление.
Осталось только вызвать метод getNext из обратного вызова кнопки.
lib/main.dart
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
Сохраните и попробуйте приложение. При каждом нажатии кнопки Next (Далее) должна генерироваться новая случайная пара слов.
В следующем разделе вы сделаете пользовательский интерфейс более привлекательным.
5. Сделайте приложение красивее
Вот как приложение выглядит сейчас.

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

Как извлечь виджет
Строка, отвечающая за показ текущей пары слов, теперь выглядит так: Text(appState.current.asLowerCase). Чтобы сделать его более сложным, рекомендуется извлечь эту строку в отдельный виджет. Использование отдельных виджетов для отдельных логических частей пользовательского интерфейса – важный способ управления сложностью во Flutter.
В Flutter есть помощник по рефакторингу для извлечения виджетов, но перед его использованием убедитесь, что извлекаемая строка обращается только к тому, что ей нужно. Сейчас строка обращается к appState, но на самом деле ей нужно знать только текущую пару слов.
Поэтому перепишите виджет MyHomePage следующим образом:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← Add this.
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'),
Text(pair.asLowerCase), // ← Change to this.
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
Хорошо. Виджет "Text" больше не относится ко всему разделу "appState".
Теперь откройте меню Refactor (Рефакторинг). В VS Code это можно сделать одним из двух способов:
- Нажмите правой кнопкой мыши на фрагмент кода, который нужно изменить (в данном случае
Text), и в раскрывающемся меню выберите Refactor... (Рефакторинг...).
ИЛИ
- Переместите курсор на фрагмент кода, который нужно рефакторить (в данном случае
Text), и нажмитеCtrl+.(Windows/Linux) илиCmd+.(Mac).
В меню Refactor (Рефакторинг) выберите Extract Widget (Извлечь виджет). Укажите название, например BigCard, и нажмите Enter.
В конце текущего файла будет автоматически создан новый класс BigCard. Класс выглядит примерно так:
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
// ...
Обратите внимание, что приложение продолжает работать даже во время рефакторинга.
Добавление карты
Теперь давайте сделаем этот виджет таким, каким мы его задумали в начале этого раздела.
Найдите класс BigCard и метод build() в нем. Как и ранее, вызовите меню Refactor (Рефакторинг) для виджета Text. Однако на этот раз вы не будете извлекать виджет.
Вместо этого выберите Wrap with Padding (Обернуть с отступом). Будет создан новый родительский виджет Padding, который будет содержать виджет Text. После сохранения вы увидите, что случайное слово уже имеет больше места для дыхания.
Увеличьте отступ, заданный по умолчанию (8.0). Например, используйте 20, чтобы увеличить отступы.
Теперь перейдем на уровень выше. Наведите курсор на виджет Padding, откройте меню Refactor (Рефакторинг) и выберите Wrap with widget... (Обернуть виджетом).
Это позволяет указать родительский виджет. Введите "Карточка" и нажмите Ввод.
Это обернет виджет Padding, а значит и Text, виджетом Card.
lib/main.dart
// ...
class BigCard extends StatelessWidget {
const BigCard({super.key, required this.pair});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
}
// ...
Теперь приложение будет выглядеть примерно так:

Тема и стиль
Чтобы карточка выделялась, выберите более насыщенный цвет. Чтобы сохранить единообразие цветовой схемы, используйте Theme приложения.
Внесите следующие изменения в метод build() класса BigCard.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← Add this.
return Card(
color: theme.colorScheme.primary, // ← And also this.
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
// ...
Эти две новые строки выполняют большую работу:
- Сначала код запрашивает текущую тему приложения с помощью
Theme.of(context). - Затем код определяет цвет карточки, который должен совпадать со свойством
colorSchemeтемы. Цветовая схема содержит много цветов, иprimary– самый заметный, определяющий цвет приложения.
Теперь карточка окрашена в основной цвет приложения:

Вы можете изменить этот цвет и цветовую схему всего приложения, прокрутив вверх до MyApp и изменив там исходный цвет для ColorScheme.
Обратите внимание, как плавно меняется цвет. Это называется неявной анимацией. Многие виджеты Flutter плавно интерполируют значения, чтобы интерфейс не просто "прыгал" между состояниями.
Цвет кнопки под карточкой также меняется. В этом и заключается преимущество использования Theme на уровне приложения по сравнению с заданными в коде значениями.
TextTheme
Но у карточки все ещё есть проблема: текст слишком мелкий, а его цвет затрудняет чтение. Чтобы исправить это, внесите следующие изменения в метод build() класса BigCard.
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ Add this.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Change this line.
child: Text(pair.asLowerCase, style: style),
),
);
}
// ...
Почему мы вносим это изменение
- Используя
theme.textTheme, вы получаете доступ к теме шрифта приложения. Этот класс включает такие элементы, какbodyMedium(для стандартного текста среднего размера),caption(для подписей к изображениям) илиheadlineLarge(для крупных заголовков). - Свойство
displayMedium– это крупный стиль, предназначенный для отображения текста. Слово display используется здесь в типографском смысле, как в display typeface. В документации поdisplayMediumговорится, что "стили отображения зарезервированы для короткого важного текста" – именно то, что нам нужно. - Свойство
displayMediumтемы теоретически может бытьnull. Dart, язык программирования, на котором вы пишете это приложение, является нулевым, поэтому он не позволит вам вызывать методы объектов, которые потенциально являютсяnull. В этом случае вы можете использовать оператор!, чтобы Dart знал, что вы делаете. (В этом случаеdisplayMediumточно не null. Объяснение того, как мы это узнали, выходит за рамки этой практической работы. - Вызов
copyWith()дляdisplayMediumвозвращает копию стиля текста с заданными вами изменениями. В этом случае вы меняете только цвет текста. - Чтобы получить новый цвет, вы снова обращаетесь к теме приложения. Свойство
onPrimaryцветовой схемы определяет цвет, который хорошо подходит для использования поверх основного цвета приложения.
Приложение должно выглядеть примерно так:

При желании вы можете изменить карточку. Вот несколько полезных советов:
copyWith()позволяет менять не только цвет текста, но и другие его параметры. Чтобы получить полный список свойств, которые можно изменить, поместите курсор в любое место внутри скобокcopyWith()и нажмитеCtrl+Shift+Space(Windows/Linux) илиCmd+Shift+Space(Mac).- Аналогичным образом можно изменить другие параметры виджета
Card. Например, вы можете увеличить тень карточки, увеличив значение параметраelevation. - Попробуйте поэкспериментировать с цветами. Помимо
theme.colorScheme.primary, есть ещё.secondary,.surfaceи множество других. У всех этих цветов есть эквиваленты вonPrimary.
Улучшение специальных возможностей
Flutter по умолчанию делает приложения доступными. Например, каждое приложение Flutter правильно отображает весь текст и интерактивные элементы в приложении для программ чтения с экрана, таких как TalkBack и VoiceOver.

но иногда требуется дополнительная настройка. В случае с этим приложением у программы чтения с экрана могут возникнуть проблемы с произношением некоторых сгенерированных пар слов. Человек без труда поймет, что слово cheaphead состоит из двух частей, а программа чтения с экрана может произнести ph в середине слова как f.
Решение: замените pair.asLowerCase на "${pair.first} ${pair.second}". Второй использует интерполяцию строк, чтобы создать строку (например, "cheap head") из двух слов, содержащихся в pair. Если использовать два отдельных слова вместо одного составного, программы чтения с экрана будут правильно их распознавать, что сделает сайт более удобным для пользователей с нарушениями зрения.
Однако вы можете сохранить визуальную простоту pair.asLowerCase. Используйте свойство semanticsLabel виджета Text, чтобы переопределить визуальный контент текстового виджета семантическим контентом, который больше подходит для программ чтения с экрана:
lib/main.dart
// ...
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// ↓ Make the following change.
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
);
}
// ...
Теперь программы чтения с экрана правильно произносят каждую сгенерированную пару слов, а интерфейс остается прежним. Попробуйте использовать программу чтения с экрана на своем устройстве.
Центрировать интерфейс
Теперь, когда случайная пара слов представлена с достаточным визуальным чутьем, пришло время поместить ее в центр окна/экрана приложения.
Во-первых, помните, что BigCard – это часть Column. По умолчанию столбцы группируют свои дочерние элементы вверху, но мы можем это переопределить. Перейдите к методу build() класса MyHomePage и внесите следующие изменения:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← Add this.
children: [
Text('A random AWESOME idea:'),
BigCard(pair: pair),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
);
}
}
// ...
Это центрирует дочерние элементы внутри Column вдоль его главной (вертикальной) оси.

Дочерние элементы уже выровнены по центру вдоль поперечной оси столбца (другими словами, они уже выровнены по центру по горизонтали). Но сам элемент Column не выровнен по центру элемента Scaffold. Это можно проверить с помощью инспектора виджетов.
Инспектор виджетов выходит за рамки этой практической работы, но вы можете видеть, что когда выделен Column, он не занимает всю ширину приложения. Он занимает только столько горизонтального пространства, сколько нужно его дочерним элементам.
Вы можете просто выровнять по центру сам столбец. Наведите курсор на Column, вызовите меню Refactor (Рефакторинг) (с помощью Ctrl+. или Cmd+.) и выберите Wrap with Center (Обернуть в Center).
Приложение должно выглядеть примерно так:

При желании вы можете изменить его.
- Вы можете удалить виджет
TextнадBigCard. Можно утверждать, что описательный текст ("Случайная КЛАССНАЯ идея:") больше не нужен, поскольку интерфейс понятен и без него. И так чище. - Вы также можете добавить виджет
SizedBox(height: 10)междуBigCardиElevatedButton. Так виджеты будут лучше разделены. ВиджетSizedBoxпросто занимает место и ничего не отображает. Обычно он используется для создания визуальных "пробелов".
С необязательными изменениями файл MyHomePage содержит следующий код:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
),
);
}
}
// ...
Приложение выглядит следующим образом:

В следующем разделе вы добавите возможность добавлять в избранное (или «лайкать») сгенерированные слова.
6. Добавление функций
Приложение работает и иногда даже предлагает интересные пары слов. Но когда пользователь нажимает Далее, каждая пара слов исчезает навсегда. Было бы лучше, если бы можно было "запоминать" лучшие предложения, например с помощью кнопки "Нравится".

Добавьте бизнес-логику
Прокрутите до MyAppState и добавьте следующий код:
lib/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
void getNext() {
current = WordPair.random();
notifyListeners();
}
// ↓ Add the code below.
var favorites = <WordPair>[];
void toggleFavorite() {
if (favorites.contains(current)) {
favorites.remove(current);
} else {
favorites.add(current);
}
notifyListeners();
}
}
// ...
Проверьте изменения:
- Вы добавили в
MyAppStateновое свойствоfavorites. Это свойство инициализируется пустым списком:[]. - Вы также указали, что список может содержать только пары слов:
<WordPair>[], используя обобщения. Это делает приложение более надежным: Dart даже не запустит его, если вы попытаетесь добавить в него что-то, кромеWordPair. В свою очередь, вы можете использовать списокfavorites, зная, что в нем не может быть нежелательных объектов (например,null).
- Вы также добавили новый метод
toggleFavorite(), который либо удаляет текущую пару слов из списка избранного (если она уже там есть), либо добавляет ее (если ее там ещё нет). В обоих случаях код вызываетnotifyListeners();.
Добавьте кнопку
Теперь, когда бизнес-логика готова, можно снова заняться пользовательским интерфейсом. Чтобы разместить кнопку "Нравится" слева от кнопки "Далее", требуется Row. Виджет Row – это горизонтальный эквивалент виджета Column, который вы видели ранее.
Сначала оберните существующую кнопку в Row. Перейдите к методу build() класса MyHomePage, наведите курсор на ElevatedButton, откройте меню Рефакторинг с помощью сочетания клавиш Ctrl+. или Cmd+. и выберите Обернуть в строку.
Сохранив изменения, вы заметите, что Row действует аналогично Column – по умолчанию он группирует дочерние элементы слева. (Column сгруппировал дочерние элементы вверху). Чтобы исправить это, можно использовать тот же подход, что и раньше, но с mainAxisAlignment. Однако в учебных целях используйте mainAxisSize. Это указывает Row не занимать все доступное горизонтальное пространство.
Внесите следующие изменения:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min, // ← Add this.
children: [
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
Интерфейс вернулся к прежнему виду.

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

Не страшно, если вы не сделаете это точно так же, как показано ниже. Если вы не ищете сложных задач, можете не обращать внимания на значок сердца.
Не бойтесь ошибиться – вы только начинаете работать с Flutter.

Вот один из способов добавить вторую кнопку в MyHomePage. На этот раз используйте конструктор ElevatedButton.icon(), чтобы создать кнопку со значком. А в начале метода build выберите подходящий значок в зависимости от того, есть ли текущая пара слов в избранном. Обратите внимание, что мы снова использовали SizedBox, чтобы немного разделить кнопки.
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
// ↓ Add this.
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// ↓ And this.
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
),
);
}
}
// ...
Приложение должно выглядеть следующим образом:
К сожалению, пользователь не может увидеть избранное. Пришло время добавить в наше приложение отдельный экран. Увидимся в следующем разделе!
7. Добавить боковую панель навигации
В большинстве приложений контент не помещается на одном экране. В этом приложении можно было бы так сделать, но в учебных целях вы создадите отдельный экран для избранного. Чтобы переключаться между двумя экранами, вы реализуете свой первый StatefulWidget.

Чтобы быстрее перейти к сути этого шага, разделите MyHomePage на два отдельных виджета.
Выделите весь код MyHomePage, удалите его и замените следующим кодом:
lib/main.dart
// ...
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: 0,
onDestinationSelected: (value) {
print('selected: $value');
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
class GeneratorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
IconData icon;
if (appState.favorites.contains(pair)) {
icon = Icons.favorite;
} else {
icon = Icons.favorite_border;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () {
appState.toggleFavorite();
},
icon: Icon(icon),
label: Text('Like'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: () {
appState.getNext();
},
child: Text('Next'),
),
],
),
],
),
);
}
}
// ...
После сохранения вы увидите, что визуальная часть интерфейса готова, но не работает. Нажатие на значок сердца на панели навигации ни к чему не приводит.

Проверьте изменения.
- Во-первых, обратите внимание, что все содержимое
MyHomePageизвлекается в новый виджетGeneratorPage. Единственная часть старого виджетаMyHomePage, которая не была извлечена, – этоScaffold. - В новом
MyHomePageестьRowс двумя дочерними элементами. Первый виджет –SafeArea, а второй –Expanded. SafeAreaгарантирует, что его дочерний элемент не будет скрыт аппаратным вырезом или строкой состояния. В этом приложении виджет оборачиваетNavigationRail, чтобы кнопки навигации не перекрывались, например, строкой состояния мобильного устройства.- Вы можете изменить строку
extended: falseв файлеNavigationRailнаtrue. Рядом со значками появятся ярлыки. Далее вы узнаете, как сделать так, чтобы это происходило автоматически, когда в приложении достаточно места по горизонтали. - На панели навигации есть два пункта назначения (Главная и Избранное) с соответствующими значками и ярлыками. Он также определяет текущий
selectedIndex. Индекс 0 соответствует первому пункту назначения, 1 – второму и т. д. Пока что в коде задано значение 0. - Навигационная панель также определяет, что происходит, когда пользователь выбирает один из разделов с помощью
onDestinationSelected. Сейчас приложение просто выводит запрошенное значение индекса с помощьюprint(). - Второй дочерний элемент
Row– виджетExpanded. Расширенные виджеты очень полезны в строках и столбцах. Они позволяют создавать макеты, в которых некоторые дочерние элементы занимают только необходимое им пространство (в данном случаеSafeArea), а другие виджеты должны занимать как можно больше оставшегося места (в данном случаеExpanded). ВиджетыExpandedможно назвать "жадными". Чтобы лучше понять роль этого виджета, попробуйте обернуть виджетSafeAreaдругим виджетомExpanded. Полученный макет выглядит примерно так:

- Два виджета
Expandedделят между собой все доступное горизонтальное пространство, хотя панели навигации на самом деле нужна лишь небольшая часть слева. - Внутри виджета
Expandedнаходится цветной контейнерContainer, а внутри контейнера –GeneratorPage.
Виджеты без сохранения состояния и с сохранением состояния
До сих пор MyAppState покрывал все ваши потребности в штате. Вот почему все виджеты, которые вы написали до сих пор, не имеют состояния. Они не содержат собственного изменяемого состояния. Ни один из виджетов не может изменить себя – они должны пройти через MyAppState.
Но скоро все изменится.
Вам нужен способ сохранить значение selectedIndex панели навигации. Вы также хотите иметь возможность изменять это значение в обратном вызове onDestinationSelected.
Можно добавить selectedIndex как ещё одно свойство MyAppState. И это сработало. Но вы можете себе представить, что состояние приложения быстро вырастет за пределы разумного, если каждый виджет будет хранить в нем свои значения.

Некоторые состояния относятся только к одному виджету, поэтому они должны оставаться в этом виджете.
Введите StatefulWidget – тип виджета, у которого есть State. Сначала преобразуйте MyHomePage в виджет с отслеживанием состояния.
Поместите курсор на первую строку MyHomePage (которая начинается с class MyHomePage...) и вызовите меню Рефакторинг, нажав Ctrl+. или Cmd+.. Затем выберите Convert to StatefulWidget (Преобразовать в StatefulWidget).
IDE создаст для вас новый класс _MyHomePageState. Этот класс расширяет State и поэтому может управлять собственными значениями. (Оно может меняться само.) Также обратите внимание, что метод build из старого виджета без сохранения состояния переместился в _MyHomePageState (вместо того, чтобы оставаться в виджете). Он был перенесен дословно – ничего внутри метода build не изменилось. Она просто находится в другом месте.
setState
Новому виджету с отслеживанием состояния нужно отслеживать только одну переменную: selectedIndex. Внесите следующие три изменения в файл _MyHomePageState:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0; // ← Add this property.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex, // ← Change to this.
onDestinationSelected: (value) {
// ↓ Replace print with this.
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: GeneratorPage(),
),
),
],
),
);
}
}
// ...
Проверьте изменения:
- Вы вводите новую переменную
selectedIndexи инициализируете ее значением0. - Используйте эту новую переменную в определении
NavigationRailвместо заданного в коде значения0, которое использовалось до сих пор. - Когда вызывается обратный вызов
onDestinationSelected, вместо того чтобы просто выводить новое значение в консоль, вы присваиваете егоselectedIndexвнутри вызоваsetState(). Этот вызов аналогичен методуnotifyListeners(), который использовался ранее. Он обеспечивает обновление пользовательского интерфейса.
Теперь панель навигации реагирует на действия пользователя. но размер развернутой области справа не меняется. Это связано с тем, что в коде не используется selectedIndex для определения того, какой экран отображается.
Используйте selectedIndex
Добавьте следующий код в начало метода build класса _MyHomePageState, прямо перед return Scaffold:
lib/main.dart
// ...
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
// ...
Рассмотрим следующий фрагмент кода:
- Код объявляет новую переменную
pageтипаWidget. - Затем оператор switch присваивает экран переменной
pageв соответствии с текущим значением переменнойselectedIndex. - Поскольку виджета
FavoritesPageпока нет, используйтеPlaceholder– удобный виджет, который рисует перечеркнутый прямоугольник везде, где вы его размещаете, помечая эту часть пользовательского интерфейса как незавершенную.

- Применяя принцип быстрого отказа, оператор switch также гарантирует, что будет выдана ошибка, если
selectedIndexне равно ни 0, ни 1. Это помогает предотвратить ошибки в дальнейшем. Если вы когда-нибудь добавите новый пункт назначения на панель навигации и забудете обновить этот код, программа аварийно завершит работу в процессе разработки (вместо того, чтобы позволить вам гадать, почему что-то не работает, или позволить вам опубликовать ошибочный код в рабочей среде).
Теперь, когда page содержит виджет, который вы хотите показать справа, вы, вероятно, можете догадаться, какое ещё изменение необходимо.
Вот как будет выглядеть _MyHomePageState после этого изменения:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: false,
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page, // ← Here.
),
),
],
),
);
}
}
// ...
Теперь в приложении можно переключаться между вкладкой GeneratorPage и плейсхолдером, который скоро станет страницей Избранное.
Готовность к взаимодействию
Теперь сделайте панель навигации адаптивной. То есть настройте автоматический показ ярлыков (с помощью extended: true), когда для них достаточно места.

Flutter предоставляет несколько виджетов, которые помогают сделать приложения автоматически адаптивными. Например, Wrap – это виджет, похожий на Row или Column, который автоматически переносит дочерние элементы на следующую строку, если не хватает места по вертикали или горизонтали. Есть FittedBox – виджет, который автоматически подгоняет дочерний элемент под доступное пространство в соответствии с вашими спецификациями.
Однако NavigationRail не показывает ярлыки автоматически, когда места достаточно, потому что не может определить, сколько места достаточно в каждом конкретном случае. Решение остается за вами.
Допустим, вы решили показывать ярлыки, только если ширина MyHomePage составляет не менее 600 пикселей.
В этом случае используется виджет LayoutBuilder. Он позволяет менять дерево виджетов в зависимости от доступного пространства.
Снова используйте меню Рефакторинг Flutter в VS Code, чтобы внести необходимые изменения. Однако на этот раз все немного сложнее:
- В методе
buildкласса_MyHomePageStateнаведите курсор наScaffold. - Вызовите меню Refactor (Рефакторинг), нажав
Ctrl+.(Windows/Linux) илиCmd+.(Mac). - Выберите Wrap with Builder (Обернуть с помощью Builder) и нажмите Ввод.
- Измените название добавленного элемента
BuilderнаLayoutBuilder. - Измените список параметров обратного вызова с
(context)на(context, constraints).
Обратный вызов builder для LayoutBuilder выполняется каждый раз, когда меняются ограничения. Это происходит, например, в следующих случаях:
- пользователь изменяет размер окна приложения;
- Пользователь поворачивает телефон из вертикального режима в горизонтальный или наоборот.
- Виджет рядом с
MyHomePageувеличивается в размере, уменьшая ограниченияMyHomePage
Теперь ваш код может решить, показывать ли ярлык, запросив текущее значение constraints. Внесите следующее изменение в метод build класса _MyHomePageState:
lib/main.dart
// ...
class _MyHomePageState extends State<MyHomePage> {
var selectedIndex = 0;
@override
Widget build(BuildContext context) {
Widget page;
switch (selectedIndex) {
case 0:
page = GeneratorPage();
break;
case 1:
page = Placeholder();
break;
default:
throw UnimplementedError('no widget for $selectedIndex');
}
return LayoutBuilder(builder: (context, constraints) {
return Scaffold(
body: Row(
children: [
SafeArea(
child: NavigationRail(
extended: constraints.m>axWidth = 600, // ← Here.
destinations: [
NavigationRailDestination(
icon: Icon(Icons.home),
label: Text('Home'),
),
NavigationRailDestination(
icon: Icon(Icons.favorite),
label: Text('Favorites'),
),
],
selectedIndex: selectedIndex,
onDestinationSelected: (value) {
setState(() {
selectedIndex = value;
});
},
),
),
Expanded(
child: Container(
color: Theme.of(context).colorScheme.primaryContainer,
child: page,
),
),
],
),
);
});
}
}
// ...
Теперь ваше приложение реагирует на окружающую среду, такую как размер экрана, ориентация и платформа! Другими словами, он адаптивный!
Осталось только заменить Placeholder на настоящий экран Избранное. Об этом рассказывается в следующем разделе.
8. Добавить страницу
Помните виджет Placeholder, который мы использовали вместо страницы Избранное?
Пора это исправить.
Если вы любите приключения, попробуйте сделать этот шаг самостоятельно. Ваша цель – показать список favorites в новом виджете без сохранения состояния FavoritesPage, а затем показать этот виджет вместо Placeholder.
Вот несколько советов:
- Если вам нужен прокручиваемый
Column, используйте виджетListView. - Помните, что вы можете получить доступ к экземпляру
MyAppStateиз любого виджета с помощьюcontext.watch<MyAppState>(). - Если вы хотите попробовать новый виджет, у
ListTileесть такие свойства, какtitle(обычно для текста),leading(для значков или аватаров) иonTap(для взаимодействий). Однако вы можете добиться аналогичных эффектов с помощью уже знакомых вам виджетов. - Dart позволяет использовать циклы
forвнутри литералов коллекций. Например, еслиmessagesсодержит список строк, можно использовать следующий код:

Если же вы предпочитаете функциональное программирование, Dart позволяет писать код, например, так: messages.map((m) => Text(m)).toList(). И, конечно, вы всегда можете создать список виджетов и императивно добавить его в метод build.
Преимущество добавления страницы Избранное самостоятельно заключается в том, что вы узнаете больше, принимая собственные решения. Недостаток в том, что вы можете столкнуться с проблемой, которую пока не в состоянии решить самостоятельно. Помните: неудачи – это нормально, и это один из самых важных элементов обучения. Никто не ждет, что вы освоите Flutter за первый час, и вы тоже не должны этого ждать.

Ниже приведен один из способов реализации страницы избранного. Надеемся, что реализация вдохновит вас на эксперименты с кодом, чтобы улучшить интерфейс и сделать его уникальным.
Вот новый класс FavoritesPage:
lib/main.dart
// ...
class FavoritesPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
if (appState.favorites.isEmpty) {
return Center(
child: Text('No favorites yet.'),
);
}
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text('You have '
'${appState.favorites.length} favorites:'),
),
for (var pair in appState.favorites)
ListTile(
leading: Icon(Icons.favorite),
title: Text(pair.asLowerCase),
),
],
);
}
}
Вот что делает виджет:
- Оно получает текущее состояние приложения.
- Если список пуст, по центру экрана появится сообщение Нет избранных.
- В противном случае показывается список, который можно прокручивать.
- В начале списка будет сводка, например У вас 5 избранных мест.
- Затем код перебирает все избранные элементы и создает виджет
ListTileдля каждого из них.
Теперь осталось только заменить виджет Placeholder на FavoritesPage. Вуаля!
Итоговый код приложения можно найти в хранилище практической работы на GitHub.
9. Дальнейшие действия
Поздравляем!
Вы в центре внимания Вы взяли неработающий шаблон с виджетом Column и двумя виджетами Text и превратили его в адаптивное и удобное приложение.

Что мы обсудили
- Основы работы с Flutter
- Создание макетов во Flutter
- Связывание действий пользователя (например, нажатий кнопок) с поведением приложения.
- Как упорядочить код Flutter
- Как сделать приложение адаптивным
- Как добиться единообразного внешнего вида приложения
Что дальше
- Поэкспериментируйте с приложением, которое вы написали в ходе этой практической работы.
- Посмотрите код расширенной версии того же приложения, чтобы узнать, как добавлять анимированные списки, градиенты, перекрестные затухания и многое другое.
- Продолжайте обучение на сайте flutter.dev/learn.