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
- Андроид
- Окна
- macOS
- Линукс
- сеть
Однако общепринятой практикой является выбор одной операционной системы, для которой вы в первую очередь будете разрабатывать. Это ваша «цель разработки» — операционная система, на которой работает ваше приложение во время разработки.
Например, предположим, что вы используете ноутбук с Windows для разработки приложения Flutter. Если вы выбираете Android в качестве цели разработки, вы обычно подключаете устройство Android к ноутбуку с Windows с помощью USB-кабеля, и ваше разрабатываемое приложение запускается на этом подключенном устройстве Android. Но вы также можете выбрать Windows в качестве цели разработки, что означает, что ваше разрабатываемое приложение работает как приложение Windows вместе с вашим редактором.
Может возникнуть соблазн выбрать Интернет в качестве цели разработки. Обратной стороной этого выбора является то, что вы теряете одну из самых полезных функций разработки Flutter: горячую перезагрузку с сохранением состояния. Flutter не может выполнять горячую перезагрузку веб-приложений.
Сделайте свой выбор сейчас. Помните: позже вы всегда сможете запустить свое приложение в других операционных системах. Просто наличие четкой цели развития делает следующий шаг более плавным.
Установить флаттер
Самые актуальные инструкции по установке Flutter SDK всегда находятся на docs.flutter.dev .
Инструкции на веб-сайте Flutter охватывают не только установку самого SDK, но также инструментов, связанных с целями разработки, и плагинов редактора. Помните, что для этой лаборатории кода вам нужно установить только следующее:
- Флаттер SDK
- Код Visual Studio с плагином Flutter
- Программное обеспечение, необходимое для выбранной вами цели разработки (например: Visual Studio для Windows или Xcode для macOS).
В следующем разделе вы создадите свой первый проект Flutter.
Если у вас уже возникли проблемы, некоторые из этих вопросов и ответов (из StackOverflow) могут оказаться полезными для устранения неполадок.
Часто задаваемые вопросы
- Как мне найти путь к Flutter SDK?
- Что делать, если команда 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 Project .
Затем выберите «Приложение» , а затем папку, в которой нужно создать проект. Это может быть ваш домашний каталог или что-то вроде 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' # Remove this line if you wish to publish to pub.dev
version: 0.0.1+1
environment:
sdk: ^3.1.1
dependencies:
flutter:
sdk: flutter
english_words: ^4.0.0
provider: ^6.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.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/
.
Замените содержимое этого файла следующим:
библиотека/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(
useMaterial3: true,
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. Добавьте кнопку
На этом шаге добавляется кнопка «Далее» для создания новой пары слов.
Запустите приложение
Сначала откройте lib/main.dart
и убедитесь, что выбрано целевое устройство. В правом нижнем углу VS Code вы найдете кнопку, показывающую текущее целевое устройство. Нажмите, чтобы изменить его.
Пока lib/main.dart
открыт, найдите «play». кнопку в правом верхнем углу окна VS Code и щелкните ее.
Примерно через минуту ваше приложение запустится в режиме отладки. Это пока не так уж и много:
Первая горячая перезагрузка
В нижней части lib/main.dart
добавьте что-нибудь к строке в первом объекте Text
и сохраните файл (с помощью Ctrl+S
или Cmd+S
). Например:
библиотека/main.dart
// ...
return Scaffold(
body: Column(
children: [
Text('A random AWESOME idea:'), // ← Example change.
Text(appState.current.asLowerCase),
],
),
);
// ...
Обратите внимание, как приложение сразу меняется, но случайное слово остается прежним. Это знаменитая горячая перезагрузка Flutter с сохранением состояния в действии. Горячая перезагрузка запускается, когда вы сохраняете изменения в исходном файле.
Часто задаваемые вопросы
- Что делать, если горячая перезагрузка не работает в VSCode?
- Нужно ли мне нажимать «r» для горячей перезагрузки в VSCode?
- Работает ли горячая перезагрузка через Интернет?
- Как удалить баннер «Отладка»?
Добавление кнопки
Затем добавьте кнопку внизу Column
, прямо под вторым экземпляром Text
.
библиотека/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 показывает нажатую кнопку! сообщение.
Ускоренный курс Flutter за 5 минут
Как бы ни было интересно наблюдать за консолью отладки , вам хочется, чтобы кнопка делала что-то более значимое. Однако прежде чем перейти к этому, взгляните повнимательнее на код в lib/main.dart
, чтобы понять, как он работает.
библиотека/main.dart
// ...
void main() {
runApp(MyApp());
}
// ...
В самом верху файла вы найдете функцию main()
. В своей текущей форме он только сообщает Flutter о запуске приложения, определенного в MyApp
.
библиотека/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(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
),
home: MyHomePage(),
),
);
}
}
// ...
Класс MyApp
расширяет StatelessWidget
. Виджеты — это элементы, из которых вы создаете каждое приложение Flutter. Как видите, даже само приложение представляет собой виджет.
Код в MyApp
настраивает все приложение. Он создает состояние всего приложения (подробнее об этом позже), дает приложению имя, определяет визуальную тему и устанавливает «домашний» виджет — отправную точку вашего приложения.
библиотека/main.dart
// ...
class MyAppState extends ChangeNotifier {
var current = WordPair.random();
}
// ...
Далее класс MyAppState
определяет… ну… состояние приложения. Это ваш первый опыт работы с Flutter, поэтому эта лаборатория кода будет простой и целенаправленной. Во Flutter существует множество мощных способов управления состоянием приложения. Один из самых простых для объяснения — это ChangeNotifier
, подход, использованный в этом приложении.
-
MyAppState
определяет данные, необходимые приложению для работы. На данный момент он содержит только одну переменную с текущей парой случайных слов. Вы добавите это позже. - Класс состояния расширяет
ChangeNotifier
, что означает, что он может уведомлять других о своих собственных изменениях . Например, если текущая пара слов изменится, некоторые виджеты в приложении должны об этом узнать. - Состояние создается и предоставляется всему приложению с помощью
ChangeNotifierProvider
(см. код выше вMyApp
). Это позволяет любому виджету в приложении получать информацию о состоянии.
библиотека/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
.
библиотека/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
из обратного вызова кнопки.
библиотека/main.dart
// ...
ElevatedButton(
onPressed: () {
appState.getNext(); // ← This instead of print().
},
child: Text('Next'),
),
// ...
Сохраните и попробуйте приложение прямо сейчас. Он должен генерировать новую случайную пару слов каждый раз, когда вы нажимаете кнопку «Далее» .
В следующем разделе вы улучшите пользовательский интерфейс.
5. Сделайте приложение красивее
Вот как приложение выглядит на данный момент.
Не здорово. Центральная часть приложения — случайно сгенерированная пара слов — должна быть более заметной. В конце концов, это главная причина, по которой наши пользователи используют это приложение! Кроме того, содержимое приложения странно смещено по центру, и все приложение скучно черно-белое.
В этом разделе эти проблемы решаются путем работы над дизайном приложения. Конечная цель этого раздела примерно следующая:
Извлечь виджет
Строка, отвечающая за отображение текущей пары слов, теперь выглядит так: Text(appState.current.asLowerCase)
. Чтобы превратить ее во что-то более сложное, рекомендуется выделить эту строку в отдельный виджет. Наличие отдельных виджетов для отдельных логических частей вашего пользовательского интерфейса — важный способ управления сложностью во Flutter.
Flutter предоставляет помощник по рефакторингу для извлечения виджетов, но прежде чем использовать его, убедитесь, что извлекаемая строка обращается только к тому, что ей нужно. Прямо сейчас строка обращается к appState
, но на самом деле ей нужно знать только текущую пару слов.
По этой причине перепишите виджет MyHomePage
следующим образом:
библиотека/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
.
Теперь вызовите меню «Рефакторинг» . В VS Code это можно сделать одним из двух способов:
- Щелкните правой кнопкой мыши фрагмент кода, который вы хотите рефакторить (в данном случае
Text
), и выберите «Рефакторинг...» в раскрывающемся меню.
ИЛИ
- Переместите курсор на фрагмент кода, который вы хотите реорганизовать (в данном случае
Text
), и нажмитеCtrl+.
(Win/Linux) илиCmd+.
(Мак).
В меню «Рефакторинг» выберите «Извлечь виджет» . Назначьте имя, например BigCard , и нажмите Enter
.
Это автоматически создаст новый класс BigCard
в конце текущего файла. Класс выглядит примерно так:
библиотека/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()
внутри него. Как и раньше, вызовите меню «Рефакторинг» виджета Text
. Однако на этот раз вы не собираетесь извлекать виджет.
Вместо этого выберите «Обтекание с отступом» . Это создаст новый родительский виджет вокруг виджета Text
под названием Padding
. После сохранения вы увидите, что у случайного слова уже есть больше места для передышки.
Увеличьте отступ со значения по умолчанию 8.0
. Например, используйте что-то вроде 20
для более просторного заполнения.
Далее поднимитесь на один уровень выше. Наведите курсор на виджет Padding
, откройте меню «Рефакторинг» и выберите «Обтекание виджетом...» .
Это позволяет вам указать родительский виджет. Введите «Карта» и нажмите Enter .
Это обертывает виджет Padding
и, следовательно, Text
, виджетом Card
.
Тема и стиль
Чтобы карта выделялась больше, раскрасьте ее более насыщенным цветом. А поскольку всегда полезно сохранять единую цветовую схему, используйте Theme
приложения, чтобы выбрать цвет.
Внесите следующие изменения в метод build()
BigCard
.
библиотека/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
для всего приложения, а не жесткого кодирования значений.
Текстовая тема
С карточкой все еще есть проблема: текст слишком мелкий, а его цвет плохо читается. Чтобы это исправить, внесите следующие изменения в метод build()
BigCard
.
библиотека/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
— это большой стиль, предназначенный для отображения текста. Слово « дисплей» здесь используется в типографском смысле, например, в «дисплейном шрифте» . В документацииdisplayMedium
говорится, что «стили отображения зарезервированы для короткого важного текста» — это именно наш вариант использования. - Свойство
displayMedium
темы теоретически может иметь значениеnull
. Dart, язык программирования, на котором вы пишете это приложение, является нулевым, поэтому он не позволит вам вызывать методы объектов, которые потенциально имеютnull
. Однако в этом случае вы можете использовать!
оператор («оператор взрыва»), чтобы заверить Дарта, что вы знаете, что делаете. (В этом случаеdisplayMedium
определенно не равен нулю. Однако причина, по которой мы это знаем, выходит за рамки этой кодовой лаборатории.) - Вызов
copyWith()
вdisplayMedium
возвращает копию стиля текста с определенными вами изменениями. В этом случае вы меняете только цвет текста. - Чтобы получить новый цвет, вы снова получаете доступ к теме приложения. Свойство
onPrimary
цветовой схемы определяет цвет, который хорошо подходит для использования в качестве основного цвета приложения.
Приложение теперь должно выглядеть примерно так:
Если вам так хочется, меняйте карту дальше. Вот несколько идей:
-
copyWith()
позволяет вам изменить стиль текста гораздо больше, чем просто цвет. Чтобы получить полный список свойств, которые вы можете изменить, поместите курсор в круглые скобкиcopyWith()
и нажмитеCtrl+Shift+Space
(Win/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
, чтобы переопределить визуальное содержимое текстового виджета семантическим содержимым, более подходящим для программ чтения с экрана:
библиотека/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
и внесите следующее изменение:
библиотека/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
. Мы можем проверить это с помощью Widget Inspector .
Сам инспектор виджетов выходит за рамки этой кодовой лаборатории, но вы можете видеть, что когда Column
выделен, он не занимает всю ширину приложения. Он занимает ровно столько горизонтального пространства, сколько нужно его детям.
Вы можете просто центрировать сам столбец. Поместите курсор на Column
, вызовите меню Refactor (с помощью Ctrl+.
или Cmd+.
) и выберите Wrap with Center .
Приложение теперь должно выглядеть примерно так:
Если вы хотите, вы можете настроить это еще немного.
- Вы можете удалить виджет
Text
надBigCard
. Можно утверждать, что описательный текст («Случайная УДИВИТЕЛЬНАЯ идея:») больше не нужен, поскольку пользовательский интерфейс имеет смысл даже без него. И так чище. - Вы также можете добавить виджет
SizedBox(height: 10)
междуBigCard
иElevatedButton
. Таким образом, между двумя виджетами будет немного больше разделения. ВиджетSizedBox
просто занимает место и сам по себе ничего не отображает. Обычно его используют для создания визуальных «пробелов».
С учетом необязательных изменений MyHomePage
содержит следующий код:
библиотека/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
и добавьте следующий код:
библиотека/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
не занимать все доступное горизонтальное пространство.
Внесите следующее изменение:
библиотека/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
еще раз, чтобы две кнопки были немного раздвинуты.
библиотека/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
, удалите их и замените следующим кодом:
библиотека/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
. Выбранный индекс, равный нулю, выбирает первый пункт назначения, выбранный индекс, равный единице, выбирает второй пункт назначения и так далее. На данный момент он жестко запрограммирован на ноль. - Навигационная направляющая также определяет, что происходит, когда пользователь выбирает одно из пунктов назначения с помощью
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+.
. Затем выберите «Преобразовать в StatefulWidget» .
IDE создаст для вас новый класс _MyHomePageState
. Этот класс расширяет State
и, следовательно, может управлять своими собственными значениями. (Он может измениться сам .) Также обратите внимание, что метод build
из старого виджета без сохранения состояния переместился в _MyHomePageState
(вместо того, чтобы оставаться в виджете). Он был перенесен дословно — внутри метода build
ничего не изменилось. Теперь он просто живет где-то в другом месте.
setState
Новому виджету с состоянием необходимо отслеживать только одну переменную: selectedIndex
. Внесите следующие 3 изменения в _MyHomePageState
:
библиотека/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
для определения того, какой экран будет отображаться.
Использовать выбранныйиндекс
Поместите следующий код в начало метода build
_MyHomePageState
, непосредственно перед return Scaffold
:
библиотека/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
. - Затем оператор переключения назначает экран
page
в соответствии с текущим значением вselectedIndex
. - Поскольку
FavoritesPage
пока нет, используйтеPlaceholder
; удобный виджет, который рисует перечеркнутый прямоугольник, где бы вы его ни разместили, отмечая эту часть пользовательского интерфейса как незавершенную.
- Применяя принцип отказоустойчивости , оператор switch также гарантирует выдачу ошибки, если
selectedIndex
не равен ни 0, ни 1. Это помогает предотвратить ошибки в дальнейшем. Если вы когда-нибудь добавите новый пункт назначения в навигационную панель и забудете обновить этот код, программа выйдет из строя в процессе разработки (в отличие от того, чтобы позволить вам догадаться, почему что-то не работает, или позволить вам опубликовать ошибочный код в производство).
Теперь, когда page
содержит виджет, который вы хотите отобразить справа, вы, вероятно, можете догадаться, какие еще изменения необходимы.
Вот _MyHomePageState
после этого единственного оставшегося изменения:
библиотека/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
, который автоматически переносит дочерние элементы на следующую «строку» (называемую «run»), когда недостаточно вертикального или горизонтального пространства. Существует FittedBox
, виджет, который автоматически помещает своего дочернего элемента в доступное пространство в соответствии с вашими требованиями.
Но NavigationRail
не отображает метки автоматически , когда достаточно места, поскольку не может знать, сколько места достаточно в каждом контексте. Это решение зависит от вас, разработчика.
Допустим, вы решили показывать метки только в том случае, если ширина MyHomePage
составляет не менее 600 пикселей.
Виджет для использования, в данном случае, является LayoutBuilder
. Это позволяет изменить дерево виджетов в зависимости от того, сколько у вас есть места.
Еще раз, используйте меню Refactor Flutter в VS -коде, чтобы внести необходимые изменения. На этот раз, однако, это немного сложнее:
- Внутри метода
build
_MyHomePageState
поставьте курсор наScaffold
. - Позвоните в меню Refactor с
Ctrl+.
(Windows/Linux) илиCmd+.
(Mac). - Выберите обертку с помощью строителя и нажмите Enter .
- Измените название недавно добавленного
Builder
наLayoutBuilder
. - Измените список параметров обратного вызова от
(context)
на(context, constraints)
.
Обратный вызов builder
LayoutBuilder
называется каждый раз, когда меняются ограничения. Это происходит, когда, например:
- Пользователь изменяет размер окна приложения
- Пользователь вращает свой телефон из портретного режима в режим ландшафта или обратно
- Некоторый виджет рядом с
MyHomePage
растет в размерах, делая ограниченияMyHomePage
меньше - И так далее
Теперь ваш код может решить, показывать ли этикетку, запрашивая текущие constraints
. Сделайте следующее изменение однолинейного изменения в метод build
_MyHomePageState
:
библиотека/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.maxWidth >= 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
.
Преимущество добавления страницы избранного самостоятельно состоит в том, что вы учитесь больше, принимая свои собственные решения. Недостаток в том, что вы можете столкнуться с проблемами, которые вы еще не можете решить сами. Помните: неудача в порядке, и является одним из самых важных элементов обучения. Никто не ожидает, что вы прибиваете развитие трепетала в первый час, и вы тоже.
Далее следует лишь один из способов реализации страницы фаворитов. Как это реализовано, будет (надеюсь) вдохновить вас на поиграть с кодом - предпринимать пользовательский интерфейс и сделать его собственным.
Вот новый класс FavoritesPage
:
библиотека/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
. И вуаля!
Вы можете получить окончательный код этого приложения в CodeLab Repo на GitHub.
9. Следующие шаги
Поздравляем!
Посмотри на себя! Вы взяли нефункциональный каркас с Column
и двумя Text
виджетами и превратили его в отзывчивое, восхитительное маленькое приложение.
Что мы рассмотрели
- Основы того, как работает трепетание
- Создание макетов в Flutter
- Подключение взаимодействия пользователей (например, нажатие кнопки) к поведению приложений
- Сохранение организованного кода трепетания
- Сделать ваше приложение отзывчивым
- Достижение постоянного взгляда и ощущения вашего приложения
Что дальше?
- Больше экспериментируйте с приложением, которое вы написали во время этой лаборатории.
- Посмотрите на код этой расширенной версии того же приложения, чтобы увидеть, как вы можете добавить анимированные списки, градиенты, перекрестные переписки и многое другое.
- Следуйте своему учебному путешествию, отправившись на Flutter.dev/learn .