Введение
Данный параграф познакомит вас с основным инструментом для построения UI во Flutter — Widget
.
Виды кроссплатформенных фреймворков
Существует множество кроссплатформенных фреймворков помимо Flutter. Давайте рассмотрим, какие подходы они используют для отображения UI:
- Cordova, Ionic, PhoneGap и другие — фреймворки, основанные на Web View. В них весь пользовательский интерфейс приложения отрисовывается при помощи браузерного движка.
- React Native, Xamarin — фреймворки, основанные на нативных виджетах. Они используют платформенные реализации для отображения пользовательского интерфейса. Например, если приложение выводит текст на экран, при запуске на Android будет использован нативный виджет
TextView
из Android Framework, а на iOS —UITextView
из UIKit. - Flutter — фреймворк с собственным движком отрисовки. Он не использует нативные виджеты или Web View, а рисует всё напрямую при помощи движка Skia. Способ отображения UI во Flutter похож на то, как работают игровые движки.
Подход Flutter даёт несколько преимуществ:
- уверенность, что UI будет одинаково выглядеть на всех платформах;
- гибкость при разработке сложных интерфейсов;
- возможности для оптимизации внутри фреймворка;
- возможности по адаптации Flutter под любую платформу и ОС.
Знакомство с виджетами
Начнём с классического примера и выведем Hello, World
на экране устройства.
Обратите внимание на объекты Column
, Icon
, Text
. Каждый из них является наследником класса Widget
.
Widget
во Flutter — иммутабельное (неизменяемое) описание части пользовательского интерфейса. Они являются ключевой концепцией для построения UI.
Каждый виджет имеет свою ответственность:
Column
располагает дочерние виджеты один за другим в вертикальном направлении;Icon
отображает иконку, которая передаётся в качестве параметра (Icons.bolt
);Text
отображает строковое значение.
Виджетам Text
и Icon
необходимо передавать параметр textDirection
— иначе получите ошибку. Позже вы познакомитесь с виджетом MaterialApp
, который возьмёт эту работу на себя.
При работе с виджетами используется композиция (один виджет вкладывается в другой), а значит, мы можем представить их в виде дерева. В нашем случае оно будет совсем простым, но в реальном приложении дерево может быть огромным.
Дерево виджетов из Hello, World
примера
Если просто создать виджеты, они не будут отрисованы на экране. Чтобы это произошло, необходимо вызвать встроенную функцию runApp(Widget app)
, которая присоединяет переданный ей виджет, а точнее всё дерево виджетов, к экрану. Переданный виджет, в нашем случае Column
, становится корневым.
В большинстве приложений есть необходимость вызывать runApp
единожды. Если вы повторно вызовете эту функцию, то предыдущее дерево виджетов будет отсоединено от экрана, а новое присоединено. Однако не стоит так делать, если у вас нет чёткого понимания, зачем прибегать к такому подходу.
Splash Screen
Функция main
не вызывается моментально после запуска приложения. Требуется некоторое время для инициализации всех составляющих движка Flutter. Чтобы пользователь не смотрел в пустой экран, в это время отображается нативный Splash Screen (сплеш-скрин). Если запустить пример из предыдущего пункта на телефоне, то при запуске показывается логотип Flutter. Это и есть сплеш-скрин по умолчанию.
Сплеш-скрин можно кастомизировать — например, заменить на логотип вашего приложения. Подробнее об этом можно прочесть в документации Android и iOS. Благодаря нативному сплеш-скрину также необязательно вызывать runApp
первой же операцией в функции main
. Если необходимо, можно дождаться выполнения какой-либо асинхронной операции (например, инициализация библиотеки аналитики, получение данных из кэша или БД).
Widget
— это иммутабельное описание части пользовательского интерфейса.runApp(Widget app)
прикрепляет дерево виджетов к экрану.- До вызова
runApp
на экране будет отображаться нативный сплеш-скрин, который можно кастомизировать.
StatelessWidget
В первом примере мы создали виджеты прямо в функции main
. Как только вы попытаетесь построить чуть более сложный UI, станет понятно, что писать код таким образом крайне неудобно. Эту проблему решает StatelessWidget
.
StatelessWidget
— виджет, не имеющий состояния. Он позволяет декомпозировать UI.
Чтобы создать собственный StatelessWidget
, можно использовать сниппет stless
(работает в IDE с установленным плагином Flutter), который создаст следующий класс:
class SomeWidget extends StatelessWidget {
const SomeWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
Как видите, это просто класс, который наследуется от абстрактного класса StatelessWidget
и реализует метод build
. Из build-метода мы должны вернуть объект типа Widget
.
Необязательный параметр конструктора key
— уникальный идентификатор для Widget
. Подробнее ключи будут рассмотрены в следующих параграфах. Сейчас мы примем за правило, что возможность передать ключ должна быть у любого виджета.
У build-метода есть аргумент BuildContext context
. BuildContext
— это интерфейс, который предоставляет виджету методы, чтобы взаимодействовать с деревом элементов. К контексту и элементам мы скоро вернёмся.
Теперь давайте переделаем наш Hello, World
с использованием StatelessWidget
. Для этого необходимо создать наследник класса StatelessWidget
и перенести виджет Column
из функции main
в метод build
.
Обратите внимание, что мы использовали final-поля в StatelessWidget
, чтобы передать параметры извне. В нашей задаче это было вовсе не обязательно, но такой подход часто используется, когда мы хотим управлять параметрами или поведением виджета.
StatelessWidget
— виджет без состояния, позволяющий выделить часть UI в отдельный класс.
Во Flutter нет ограничений или чётких правил, каким образом разделять UI на виджеты. Обычно можно встретить разделение на виджеты по следующим критериям:
- выделение структурных частей, например Page, Header и Content;
- выделение UI-компонентов (как правило, их собирают дизайнеры в UI-kit), например AppBar, Button и TextField;
- выделение виджетов, используемых в разных частях экрана/приложения, — к примеру, виджет для отображения пользовательского комментария под постом;
- выделение декорирующих виджетов, чтобы упростить понимание вёрстки.
Посмотрите на код ниже. Мы добавили обёртку для иконки с градиентом и скруглением, но это сильно затруднило чтение build-метода.
Мы можем вынести часть кода, отвечающего за декорирование иконки, в отдельный виджет и тем самым упростить понимание кода вёрстки:
Мы сделали виджет _GradientBackground
приватным. Так делают, если уверены, что виджет будет использоваться только внутри этого файла. Если виджет пригодится в других частях приложения, то его следует сделать публичным и вынести в отдельную папку — например, lib/widgets
. Желательно заранее продумать точки расширения вашего виджета и сделать его максимально гибким.
💡 Совет: декомпозируйте виджеты так, чтобы вы могли с одного взгляда на build-метод понять, какая ответственность у этого виджета.
StatefulWidget
StatelessWidget
— хороший инструмент для декомпозиции UI. Однако приложения содержат большое количество изменяющихся данных, которые образуют состояние. В этом пункте мы рассмотрим виджет, позволяющий изменять состояние приложения.
StatefulWidget
— виджет, имеющий изменяемое состояние.
Создать StatefulWidget
можно, используя сниппет stful
. Либо, если у вас уже есть StatelessWidget
и вы хотите превратить его в Stateful, это можно быстро сделать при помощи хоткея:
- установите курсор на имени класса вашего
StatelessWidget
; - нажмите сочетание клавиш:
- DartPad/AndroidStudio/IDEA:
Alt + Enter
на Windows,Option + Enter
на macOS; - VS Code:
Ctrl + .
на Windows иCmd + .
на macOS;
- DartPad/AndroidStudio/IDEA:
- выберите в появившейся подсказке
Convert to StatefulWidget
.
Можете попробовать здесь:
Посмотрим, что получилось:
import 'package:flutter/widgets.dart';
class DummyWidget extends StatefulWidget {
final String text;
const DummyWidget({
required this.text,
super.key,
});
@override
State<DummyWidget> createState() => _DummyWidgetState();
}
class _DummyWidgetState extends State<DummyWidget> {
@override
Widget build(BuildContext context) {
return Text(widget.text);
}
}
Теперь виджет унаследован от StatefulWidget
, и в нём появилась реализация метода createState
. Flutter вызывает этот метод для создания объекта State
.
Ниже в коде располагается сам класс состояния. Реализация build-метода перемещается в него.
Иммутабельные свойства, которые мы заполняем через конструктор, так и остались в самом виджете. Но теперь для доступа к ним из build-метода нужно использовать геттер widget
.
Изменяем состояние
В первом примере использование StatefulWidget
не оправдывает себя, так как состояние пустое. Возможно, вы видели счётчик нажатий, когда создавали новый проект Flutter, поэтому давайте посмотрим на другой пример. Реализуем лампочку с выключателем.
Разберём код состояния _BulbState
. В зависимости от значения поля _isLightOn
выбираем нужный цвет фона, цвет иконки и текст на кнопке. Изменяется состояние при помощи обработчика нажатия onPressed
у виджета ElevatedButton
. При нажатии на кнопку происходит следующее:
- Вызываем метод
setState
для изменения состояния. - Меняем значение поля
_isLightOn
на противоположное. - В
_BulbState
вызывается build-метод. Возвращаем новые виджеты, используя новое значение_isLightOn
. Фреймворк встраивает новые виджеты на экран, и на экране виден актуальный UI.
setState
— метод, определённый у State
, который уведомляет фреймворк о том, что State
изменился и его нужно перерисовать.
Так, основная идея декларативного UI во Flutter заключается в том, что для изменения UI нужно создавать новые экземпляры виджетов, а не изменять имеющиеся.
Advanced setState
Теперь рассмотрим пример посложнее. Реализуем экран для ввода пин-кода. Такие часто встречаются, например, в банковских приложениях.
Мы реализовали два StatefulWidget
— ThemedApp
и _PinCode
.
Виджет ThemedApp
похож на виджет Bulb
из предыдущего примера, но теперь мы используем виджет Switch
для изменения состояния. А состояние влияет на то, какой режим темы (светлый или тёмный) будет использоваться виджетом MaterialApp
.
Второй StatefulWidget _PinCode
отвечает за ввод пин-кода. Его State
содержит поле _enteredPin
, где в виде строки хранится введённый на данный момент пин-код, и _status
, в котором в виде enum
находится то, в каком состоянии сейчас должен быть экран (ввод, правильный/неправильный пин). Когда пользователь нажимает на кнопки c цифрами, значения полей в State
меняются и при помощи вызова setState
виджет перерисовывается.
Обратите внимание, что setState
инициирует перерисовки именно у того виджета, у которого он был вызван. То есть при setState
у _PinCodeState
не будет вызван build-метод у _ThemedApp
. Вы можете сами проверить это, добавив логирование функцией print
в build-методы. За счёт разделения виджетов можно оптимизировать количество и глубину перерисовок.
Также посмотрим на метод _resetStateAfterDelay
. Он изменяет состояние спустя пять секунд после ввода пин-кода. За это время виджет может удалиться из дерева. Такое бывает, например, если в приложении произойдет навигация на предыдущий экран или состояние родительского изменится так, что он не вернёт ваш виджет из build-метода. В таком случае вызов setState
выбросит ошибку. Чтобы этого не произошло, нужно выполнить при помощи геттера mounted
проверку, что виджет по-прежнему прикреплен к дереву.
Изменение полей состояния через функцию, передаваемую в setState
, может дать ощущение, что Flutter что-то знает о полях нашего состояния. На самом деле это не так. Независимо от того, обновите вы что-то внутри setState
или нет, на следующем кадре будет вызван build-метод. Можно найти подтверждение этому в исходном коде метода setState
. Два следующих варианта кода будут работать идентично:
void _updateState() {
setState(() {
_someField = 'Some value';
});
}
void _updateState() {
_someField = 'Some value';
setState(() {});
}
Но всё же во Flutter принят подход с обновлением полей внутри setState
, так что не следует пренебрегать этим.
Иммутабельность виджетов
Во Flutter любой виджет обязан быть иммутабельным, то есть у него не должно быть изменяемых полей. Вокруг этого принципа во многом построена внутренняя работа фреймворка, в том числе это помогает оптимизировать построение UI. Нарушать данный принцип нельзя, иначе вы получите непредвиденное поведение. Именно поэтому StatefulWidget
разделен на две части — сам виджет, содержащий только иммутабельные поля, и состояние, которое может изменяться.
Немного об Element
На самом деле Flutter практически не работает с деревом виджетов. Это абстракция для удобства разработчиков. На основе дерева виджетов Flutter строит дерево элементов (Element Tree), и уже с ним идёт основная работа. Элемент — представление виджета в дереве элементов, они отвечают за жизненный цикл UI и его эффективное обновление.
Когда обновляется состояние и мы отдаём новые виджеты, Flutter сопоставляет их с имеющимся деревом элементов и добавляет/удаляет/обновляет элементы в нём.
Выше мы говорили, что BuildContext
— интерфейс для доступа к Element
. Рассмотрим подробнее, как это работает:
- мы создаём виджет;
- Flutter вызывает у этого виджета метод
createElement
; - полученный элемент помещается в Element Tree;
- у виджета (или у
State
в случаеStatefulWidget
) вызывается методbuild
, куда передаётсяElement
в качестве параметра.
Таким образом, мы имеем доступ к дереву элементов. Благодаря интерфейсу BuildContext
нам доступны только определённые методы и поля. В этом параграфе мы ещё рассмотрим, как именно можно использовать возможности, которые даёт BuildContext
.
Помимо дерева элементов, существует RenderObject Tree. Оно создаётся на основе дерева элементов и отвечает непосредственно за отрисовку и позиционирование элементов UI.
Связь деревьев Widget, Element и RenderObject
Element и RenderObject — довольно сложная тема, которая будет рассмотрена в одном из следующих параграфов. На данном этапе важно понять, что благодаря такому подходу достигается эффективная работа UI — пересоздаются только легковесные Widget, а тяжелые Element
и RenderObject
переиспользуются, если это возможно.
Кстати, метод setState
работает как раз с Element
. State вызывает метод markNeedsBuild
у Element
, с которым они связаны. Для простоты чтения из кода убраны assert
, так как они выполняются только в debug-режиме:
@protected
void setState(VoidCallback fn) {
// ... asserts ...
final Object? result = fn() as dynamic;
// ... asserts ...
_element!.markNeedsBuild();
}
А Element уже меняет у себя значение свойства _dirty
на true и добавляет себя в список «грязных» элементов. Под «грязными» подразумеваются элементы, которые нужно перерисовать на следующем кадре.
void markNeedsBuild() {
if (_lifecycleState != _ElementLifecycle.active) {
return;
}
// ... asserts ...
if (dirty) {
return;
}
_dirty = true;
owner!.scheduleBuildFor(this);
}
InheritedWidget
В предыдущем примере мы смогли легко поменять тему всего приложения. Мы всего лишь изменили один параметр themeMode
у виджета MaterialApp
, и при этом поменялись цвета всех дочерних виджетов (цвет фона, текста, Switch
), хотя больше нигде явно не передавали информацию о теме. Такое возможно благодаря InheritedWidget
.
InheritedWidget
— виджет, позволяющий эффективно передавать данные вниз по дереву.
Если бы не было InheritedWidget
, то для передачи любых данных от родительского виджета к дочернему пришлось бы передавать всю информацию через конструктор. Иногда бывают данные, которые нужны по всему приложению, например информация о направлении текста (слева направо или справа налево), информация о теме (цвета, шрифты и так далее). Такие данные было бы проблемно передавать через конструктор, потому что пришлось бы добавлять их во все виджеты — от корневого до самого последнего в дереве.
Посмотрим на пример реализации InheritedWidget
.
Виджет InheritedNumber
унаследован от InheritedWidget
. К реализации есть два требования:
- Конструктор должен содержать параметр
child
, чтобы было возможно передать дочерний виджет. - Обязательна реализация метода
updateShouldNotify
. Этот метод позволяет определить, когда дочерние виджеты будут уведомлены при измененииInheritedWidget
.
Когда какой-то InheritedWidget
встроен в дерево, дочерние виджеты могут подписаться на него при помощи метода dependOnInheritedWidgetOfExactType
у BuildContext
, который приходит в качестве параметра build-метода. Он возвращает родительский InheritedWidget
нужного типа. Чтобы упростить работу с InheritedWidget
, принято добавлять статичные методы of
и maybeOf
.
InheritedWidget могут использоваться довольно часто, например для доступа к теме, локализации. При каждом обращении к InheritedWidget
будет вызываться context.dependOnInheritedWidgetOfExactType
. Так как деревья элементов в реальных приложениях могут быть огромными, разработчики Flutter позаботились об оптимизации. Доступ к InheritedWidget
осуществляется за O(1), то есть сложность доступа не зависит от размера дерева элементов. Это достигается благодаря тому, что в каждом Element хранится Map
со ссылками на InheritedWidget
, где в качестве ключа используется тип InheritedWidget
.
При использовании InheritedWidget
важно понимать, с каким именно BuildContext
вы работаете. Посмотрите на следующий код:
class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InheritedNumber(
number: 1,
child: Text(InheritedNumber.of(context).number.toString()),
)
}
}
В этом примере мы не сможем получить InheritedNumber
, потому что dependOnInheritedWidgetOfExactType
возвращает InheritedWidget
, находящийся выше по дереву, чем context
. Мы использовали context
от SomeWidget
, а InheritedNumber
определён по дереву ниже.
Исправить приведённый код можно двумя способами. Первый вариант — вынести Text
в отдельный виджет, как было в примере из начала данного пункта. Второй вариант — использовать виджет Builder
:
class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return InheritedNumber(
number: 1,
child: Builder(
builder: (context) => Text(
InheritedNumber.of(context).number.toString(),
),
),
)
}
}
Builder позволяет получить BuildContext
именно из того места дерева, в которое он встроен.
При этом из of
и maybeOf
необязательно возвращать сам InheritedWidget
. Вы можете захотеть абстрагировать данные от самого виджета или выполнить какую-то дополнительную логику. Такое часто встречается в самом фреймворке. Посмотрим на реализацию Theme.of
:
static ThemeData of(BuildContext context) {
final _InheritedTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
final MaterialLocalizations? localizations = Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
final ScriptCategory category = localizations?.scriptCategory ?? ScriptCategory.englishLike;
final ThemeData theme = inheritedTheme?.theme.data ?? _kFallbackTheme;
return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
}
InheritedWidget
лежит в основе многих стандартных виджетов и подходов и используется в реализации многих библиотек для state management. Даже если не будете часто создавать свои InheritedWidget
, вы будете часто ими пользоваться.
InheritedWidget
— виджет, позволяющий передать данные вниз по дереву виджетов.context.dependOnInheritedWidgetOfExactType
возвращаетInheritedWidget
определённого типа, если тот существует выше по дереву виджетов, и делает это за константное время.
Жизненный цикл StatefulWidget
А теперь вернёмся к StatefulWidget
, а точнее к его жизненному циклу. Знания об InheritedWidget
были необходимы, чтобы вы могли понять все нюансы работы StatefulWidget
.
StatelessWidget
обладает совсем простым жизненным циклом. По сути у него есть только конструктор и build-метод. Его нельзя изменить, можно только создать новый виджет.
Со StatefulWidget ситуация сложнее. У State есть множество методов, которые можно реализовать для реакции на изменение состояния жизненного цикла.
Давайте посмотрим на общую схему работы StatefulWidget, а затем подробно разберём каждое состояние.
createState
createState
— метод, определённый в StatefulWidget
.
Когда вызывается
Вызывается, когда виджет встраивается в дерево, чтобы создать State
. Может быть вызван несколько раз, например если используете один и тот же экземпляр StatefulWidget
в разных местах либо если отсоединяете и снова присоединяете виджет к дереву.
Использование
createState
обязательно нужно реализовать, и он всегда выглядит одинаково:
class SomeWidget extends StatefulWidget {
@override
State<SomeWidget> createState() => _SomeWidgetState();
}
initState
Когда вызывается
initState
вызывается фреймворком после того, как State
привязывается к Element
. Гарантируется, что он вызывается только один раз для экземпляра State
.
Использование
В initState
обычно выполняется инициализация контроллеров либо полей, которые зависят от данных из widget
или context
.
Необходимо вызывать родительскую реализацию super.initState
первой операцией.
late int _someField;
late TextEditingController _messageController;
@override
void initState() {
super.initState();
_someField = widget.someField;
_loginController = TextEditingController();
// ...
}
Что нельзя делать
Несмотря на то, что из initState уже доступен context
, нельзя использовать context.dependOnInheritedWidgetOfExactType
, а соответственно, и методы SomeInheritedWidget.of(context)
.
didChangeDependencies
Когда вызывается
didChangeDependencies
вызывается фреймворком при изменении InheritedWidget
, от которых зависит State
(через context.dependOnInheritedWidgetOfExactType
). Метод InheritedWidget.updateShouldNotify
как раз нужен, чтобы определить, в каких случаях будут уведомлены зависимые виджеты.
Также этот метод будет вызван сразу после initState
. И в отличие от предыдущего метода в didChangeDependencies
уже можно использовать InheritedWidget
.
Использование
В didChangeDependencies
нужно выполнять работу, которая должна происходить при изменении InheritedWidget. Например, вы можете увидеть, что значение в InheritedWidget
изменилось, и поменять какие-то значения в State
или вызвать какой-то метод.
@override
void didChangeDependencies() {
// число будет выведено в консоль при добавлении виджета в дерево и каждый раз при обновлении InheritedNumber.
print(InheritedNumber.of(context).number);
}
build
Когда вызывается
build
вызывается для получения конфигурации виджетов. Гарантируется, что build
вызывается уже после того, как были вызваны setState
и didChangeDependencies
. Этот метод может вызываться фреймворком множество раз — например:
- после вызова
setState
; - при изменении
InheritedWidget
, от которых зависитState
; - при hot reload;
- при изменении конфигурации
StatefulWidget
.
Использование
Необходимо вернуть виджет, как мы уже делали в предыдущих примерах этого параграфа.
@override
Widget build(BuildContext context) {
return Placeholder();
}
Что нельзя делать
Из build
нельзя вызывать setState
. Если вы попытаетесь так делать, то получите следующую ошибку:
setState() or markNeedsBuild() called during build.
Вообще лучше избегать ситуаций, когда виджет обновляется из build-метода, но иногда это необходимо. К примеру, при реализации виджетов со сложной логикой отображения. Для таких случаев существует метод addPostFrameCallback
, который вызовет переданную ему функцию после текущего кадра:
@override
Widget build(BuildContext context) {
if (someCondition) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
// ...
});
});
}
return Placeholder();
}
Но будьте осторожны, чтобы не получить циклический rebuild. Поэтому в примере выше addPostFrameCallback
вызывается только по некоторому условию.
Также из build-метода не рекомендуется вызывать какие-либо методы инициализации или логирования событий в аналитику, так как build может вызываться довольно часто и непредсказуемо. Лучше использовать для этого слой бизнес-логики или другие методы жизненного цикла.
didUpdateWidget
Когда вызывается
didUpdateWidget
вызывается фреймворком, когда изменяется конфигурация виджета. Как это происходит:
- Имеется родительский виджет
ParentWidget
и дочерний StatefulWidgetChildWidget
. - У родительского
ParentWidget
происходит rebuild, например из-за вызоваsetState
. - Из build-метода
ParentWidget
снова возвращаетсяChildWidget
. - Flutter понимает, что виджет того же типа находится в том же месте и переиспользует имеющиеся
Element
иState
. - У
_ChildWidgetState
вызываетсяdidUpdateWidget
, куда передаётся старый экземплярChildWidget
. - Внутри реализации можно выполнить какую-то работу. К примеру, можно сравнить значения полей в текущей конфигурации виджета через геттер
widget
и в старой конфигурации через параметрoldWidget
и выполнить какие-то действия. Как пример — запустить анимацию. - После у
_ChildWidgetState
будет вызван build-метод.
didUpdateWidget
будет вызван каждый раз, когда изменяется экземпляр виджета, привязанный к State, независимо от того, меняются ли в нём какие-то параметры.
Использование
Можно выполнить работу, зависящую от конфигурации виджета. Например, вы можете анимированно изменить состояние виджета.
@override
void didUpdateWidget(covariant ChildWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.enabled != oldWidget.enabled) {
_animationController.animateTo(widget.enabled ? 1 : 0);
}
}
dispose
Когда вызывается
dispose
вызывается, когда виджет навсегда убирается из дерева, если фреймворк уверен, что текущий экземпляр State
больше никогда не будет переиспользован и присоединён к элементу. Гарантируется, что dispose
будет вызван только один раз после всех остальных методов жизненного цикла.
Использование
Можно воспринимать dispose
как обратное действие к initState
и использовать этот метод для освобождения ресурсов: для отмены подписок, закрытия различных Controller и так далее.
@override
void dispose() {
_someTextController.dispose();
_someSubscription?.cancel();
// Прочие действия по освобождению ресурсов
super.dispose();
}
Что нельзя делать
Так как виджет уже отсоединён от дерева, в dispose
нельзя выполнять работу с BuildContext
.
mounted
После создания State
связывается с Element
. У State
есть геттер mounted, который позволяет определить, связан ли State
в данный момент с Element
. Когда виджет убирается из дерева виджетов, State
отсоединяется от Element
.
Геттер mounted
бывает полезен, если мы хотим асинхронно вызватьsetState
. Дело в том, что вызов setState
, когда State
уже отсоединен, ведёт к ошибке. В коде это будет выглядеть следующим образом:
// ... Some Stateful Widget ...
Button(
onTap: () {
Future.delayed(const Duration(seconds: 5), () {
if (mounted) {
setState(() {
// ...
});
}
});
},
),
Заключение
В этом параграфе вы познакомились с основами работы с виджетами во Flutter. В следующем мы рассмотрим стандартные виджеты, которые предоставляет Flutter для построения сложного UI. Там будет меньше теории и больше практики.