2.7. Widgets: basics, stless, stful, inherited

Введение

Данный параграф познакомит вас с основным инструментом для построения 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 на экране устройства.

Попробуйте запустить пример не только в DartPad, но и на эмуляторе или мобильном устройстве.

Обратите внимание на объекты Column, Icon, Text. Каждый из них является наследником класса Widget.

Widget во Flutter — иммутабельное (неизменяемое) описание части пользовательского интерфейса. Они являются ключевой концепцией для построения UI.

Каждый виджет имеет свою ответственность:

  • Column располагает дочерние виджеты один за другим в вертикальном направлении;
  • Icon отображает иконку, которая передаётся в качестве параметра (Icons.bolt);
  • Text отображает строковое значение.

Виджетам Text и Icon необходимо передавать параметр textDirection — иначе получите ошибку. Позже вы познакомитесь с виджетом MaterialApp, который возьмёт эту работу на себя.

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

fluttern

Дерево виджетов из Hello, World примера

Если просто создать виджеты, они не будут отрисованы на экране. Чтобы это произошло, необходимо вызвать встроенную функцию runApp(Widget app), которая присоединяет переданный ей виджет, а точнее всё дерево виджетов, к экрану. Переданный виджет, в нашем случае Column, становится корневым.

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

Splash Screen

Функция main не вызывается моментально после запуска приложения. Требуется некоторое время для инициализации всех составляющих движка Flutter. Чтобы пользователь не смотрел в пустой экран, в это время отображается нативный Splash Screen (сплеш-скрин). Если запустить пример из предыдущего пункта на телефоне, то при запуске показывается логотип Flutter. Это и есть сплеш-скрин по умолчанию.

fluttern

Сплеш-скрин можно кастомизировать — например, заменить на логотип вашего приложения. Подробнее об этом можно прочесть в документации 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, это можно быстро сделать при помощи хоткея:

  1. установите курсор на имени класса вашего StatelessWidget;
  2. нажмите сочетание клавиш:
    1. DartPad/AndroidStudio/IDEA: Alt + Enter на Windows, Option + Enter на macOS;
    2. VS Code: Ctrl + . на Windows и Cmd + . на macOS;
  3. выберите в появившейся подсказке 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. При нажатии на кнопку происходит следующее:

  1. Вызываем метод setState для изменения состояния.
  2. Меняем значение поля _isLightOn на противоположное.
  3. В _BulbState вызывается build-метод. Возвращаем новые виджеты, используя новое значение _isLightOn. Фреймворк встраивает новые виджеты на экран, и на экране виден актуальный UI.

setState — метод, определённый у State, который уведомляет фреймворк о том, что State изменился и его нужно перерисовать.

Так, основная идея декларативного UI во Flutter заключается в том, что для изменения UI нужно создавать новые экземпляры виджетов, а не изменять имеющиеся.

fluttern

Advanced setState

Теперь рассмотрим пример посложнее. Реализуем экран для ввода пин-кода. Такие часто встречаются, например, в банковских приложениях.

Мы реализовали два StatefulWidgetThemedApp и _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. Рассмотрим подробнее, как это работает:

  1. мы создаём виджет;
  2. Flutter вызывает у этого виджета метод createElement;
  3. полученный элемент помещается в Element Tree;
  4. у виджета (или у State в случае StatefulWidget) вызывается метод build, куда передаётся Element в качестве параметра.

Таким образом, мы имеем доступ к дереву элементов. Благодаря интерфейсу BuildContext нам доступны только определённые методы и поля. В этом параграфе мы ещё рассмотрим, как именно можно использовать возможности, которые даёт BuildContext.

Помимо дерева элементов, существует RenderObject Tree. Оно создаётся на основе дерева элементов и отвечает непосредственно за отрисовку и позиционирование элементов UI.

fluttern

Связь деревьев 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. К реализации есть два требования:

  1. Конструктор должен содержать параметр child, чтобы было возможно передать дочерний виджет.
  2. Обязательна реализация метода 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, а затем подробно разберём каждое состояние.

fluttern

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 и дочерний StatefulWidget ChildWidget.
  • У родительского 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. Там будет меньше теории и больше практики.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф2.6. Dart: concurrency, изоляты
Следующий параграф2.8. Widgets: standard widgets