Состояние
stateless, stateful и что выбирать
Что это
Виджеты бывают двух видов: те, что просто рисуют переданные данные, и те, что хранят что-то своё и перерисовываются при изменении.
Идея та же, что пропсы и состояние в React (статья 10.3), и правила принятия решений те же. Отличается только синтаксис — и он во Flutter многословнее.
Как выглядит в коде
Stateless — своих данных нет
class CityCard extends StatelessWidget {
final String name;
final double? temp;
final void Function(String) onRemove;
const CityCard({
super.key,
required this.name,
this.temp,
required this.onRemove,
});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
Text(name),
Text(temp == null ? 'нет данных' : '$temp°'),
TextButton(
onPressed: () => onRemove(name),
child: const Text('Удалить'),
),
],
),
);
}
}Всё приходит снаружи через конструктор — это прямой аналог пропсов. final
у полей не случаен: менять их нельзя, как и пропсы в React.
super.key — тот самый key, что в React (статья 10.2), и нужен он для
того же: чтобы при изменении списка Flutter понимал, какой виджет какой.
Stateful — есть своё
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: _increment,
child: Text('нажато: $_count'),
);
}
}Два класса вместо одного — особенность Flutter, к которой надо привыкнуть: сам виджет и его состояние живут отдельно.
setState — единственный способ сообщить об изменении. Присваивание
_count++ без обёртки setState изменит значение, но экран не
перерисуется. Это ровно та же ошибка, что присваивание вместо setX в
React.
Подчёркивание в начале имени в Dart означает «только для этого файла».
Поэтому состояние обычно _CounterState, а поля _count.
Жизненный цикл
@override
void initState() {
super.initState();
_loadData(); // выполняется один раз при появлении
}
@override
void dispose() {
_controller.dispose(); // уборка: контроллеры, подписки, таймеры
super.dispose();
}| Метод | Когда | Аналог в React |
|---|---|---|
initState | один раз при появлении | useEffect(..., []) |
build | при каждой перерисовке | тело компонента |
dispose | при удалении | возврат функции из useEffect |
dispose обязателен для всего, что надо закрывать: контроллеры полей ввода,
прокрутки, анимации, подписки, таймеры. Забытый dispose — утечка, и
анализатор о ней не всегда предупредит.
Когда своего состояния становится много
Передавать данные через конструкторы на пять уровней вниз неудобно, и для
этого есть библиотеки управления состоянием: provider, riverpod, bloc
и другие.
Выбор между ними — архитектурное решение, а не вопрос вкуса: оно определяет, как будет устроен весь проект. Принимать его стоит самому, а не оставлять ИИ (статья 1.1), и обязательно называть выбранное в запросах — иначе получишь код под ту библиотеку, которой в обучающих данных было больше.
Как сказать ИИ
В проекте используется provider, версия 6. Состояние
хранится в ChangeNotifier. Новых библиотек управления
состоянием не добавляй.Этот виджет stateful, но своего состояния у него нет —
всё приходит через конструктор. Переделай в stateless.Проверь: у всех ли контроллеров в этом файле есть dispose.
Список того, что не закрывается.На что смотреть
Stateful без состояния. Лишний класс и лишний код. Признак: в State нет
полей, только build.
Присваивание без setState. Значение меняется, экран нет.
setState с тяжёлой работой внутри. Внутрь кладут только изменение
полей; запросы и вычисления — до него.
Нет dispose. Контроллеры и подписки живут дольше виджета.
Запрос к сети в build. build вызывается часто; запрос должен быть в
initState или в слое данных. Прямой аналог ошибки из статьи 10.4.
Библиотека состояния выбрана за тебя. В ответе появился bloc, хотя в
проекте provider, — проект получит две системы сразу.
Шпаргалка
Stateless — всё снаружи через конструктор, аналог пропсов.
Stateful — два класса: виджет и его состояние.
Менять поля только внутри
setState, иначе экран не перерисуется.initState— один раз,build— часто,dispose— уборка.Контроллеры и подписки обязаны закрываться в
dispose.Библиотеку управления состоянием выбирай сам и называй в запросе.