setState() — ключевой метод State во Flutter, уведомляющий фреймворк об изменении данных и запускающий перестроение интерфейса. По данным официальной документации Flutter (Flutter.dev, 2026), setState является основным механизмом реактивности в StatefulWidget: без его вызова UI не узнает об изменениях полей State и останется в прежнем состоянии. Метод принимает колбэк VoidCallback, внутри которого разработчик модифицирует изменяемые поля, после чего Flutter автоматически вызывает build для перестроения виджета.
Главное
setState() — встроенный метод класса State во Flutter, предназначенный для уведомления фреймворка о том, что внутреннее состояние виджета изменилось и необходимо перестроить UI. Без вызова setState Flutter не знает об изменениях — даже если поля State были модифицированы, интерфейс останется неизменным до следующего принудительного перестроения родителем.
Сигнатура метода: void setState(VoidCallback fn). Колбэк выполняется синхронно внутри setState, и только после его завершения State помечается как dirty. Это гарантирует, что все изменения применяются атомарно до перестроения. По данным Dart Language Specification (Dart Team, 2026), атомарность setState предотвращает состояние гонки, при котором build мог бы увидеть частично обновлённое состояние.
setState не принимает аргументов, не возвращает значения и не может быть переопределён. Это финальный (sealed) метод класса State. Разработчик не может изменить его поведение — только использовать по назначению. Попытка вызвать setState вне State (например, из другого класса) невозможна, так как метод объявлен в классе State.
Распространённое заблуждение — считать, что setState сам изменяет состояние. Это не так. setState всего лишь вызывает переданный колбэк (в котором разработчик изменяет поля) и затем сигнализирует фреймворку о необходимости build. Колбэк обязателен — передача null или пустого колбэка приведёт к ошибке.
Механизм работы setState() можно разбить на четыре этапа. Первый — вызов метода с колбэком. Второй — синхронное выполнение колбэка, внутри которого изменяются поля State. Третий — State помечается как dirty (грязный) в специальном поле _dirty. Четвёртый — в конце текущего микрозадачи (microtask) Flutter обходит все dirty-элементы и вызывает их build в порядке появления в дереве.
Важная деталь: setState не вызывает build немедленно. Flutter использует стратегию пакетного обновления: все dirty-элементы собираются и перестраиваются за один кадр. Это означает, что если setState вызван несколько раз в рамках одного синхронного блока, build выполнится только один раз — после завершения всех изменений. Такая оптимизация предотвращает множественные перестроения за кадр.
По данным Flutter Engine Team (Google, 2025), механизм dirty-флагов основан на проходе BuildOwner._dirtyElements. Каждый dirty StatefulElement добавляется в список и обрабатывается на этапе обновления фрейма. Если виджет был удалён из дерева до обработки, он автоматически исключается из списка dirty-элементов.
Базовый пример setState() с инкрементом счётчика. Демонстрирует корректное использование: изменение поля внутри колбэка:
class _CounterState extends State<CounterWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++; // mutating field inside callback
});
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _increment,
child: Text('$_count'),
);
}
}
Пример с текстовым полем и контроллером — setState() для управления видимостью пароля:
class _PasswordFieldState extends State<PasswordField> {
bool _obscured = true;
final _controller = TextEditingController();
void _toggleVisibility() {
setState(() {
_obscured = !_obscured;
});
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
obscureText: _obscured,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(_obscured ? Icons.visibility : Icons.visibility_off),
onPressed: _toggleVisibility,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
В этом примере setState() изменяет только булево поле _obscured, что вызывает перестроение TextField с новой иконкой и режимом отображения. Текстовый контроллер при этом не пересоздаётся — он инициализирован один раз в initState и освобождается в dispose.
Если нужно изменить несколько полей, все изменения выполняются внутри одного setState. Это гарантирует, что build увидит консистентное состояние:
setState(() {
_isLoading = false;
_items = newItems;
_error = null;
});
Три поля изменяются в одном колбэке — build выполнится один раз и увидит все изменения одновременно. Если бы каждый вызов был отдельным setState, build всё равно выполнился бы однократно благодаря пакетной обработке dirty-элементов.
Один из самых важных нюансов setState() — его поведение с асинхронными операциями. Колбэк setState выполняется синхронно, но если внутри него вызвать await, код после await выполнится уже после того, как setState завершит работу. Это означает, что изменения полей после await не будут захвачены текущим setState.
Правильный подход: асинхронная операция выполняется снаружи setState, а setState вызывается после её завершения. Весь код между получением результата и вызовом setState выполняется в синхронном контексте после await:
// CORRECT: await outside setState
Future<void> _loadData() async {
final result = await ApiService.fetchData();
setState(() {
_data = result;
_isLoading = false;
});
}
// WRONG: await inside setState — no update guarantee
void _loadDataWrong() {
setState(() async {
_data = await ApiService.fetchData(); // setState returns before await completes
_isLoading = false; // this code is not captured by setState
});
}
По данным Flutter docs (Dart async patterns, 2026), передача async-колбэка в setState является антипаттерном, так как setState ожидает VoidCallback (синхронную функцию), а async-функция возвращает Future, который игнорируется. Изменения после первого await в таком колбэке не будут корректно обработаны фреймворком.
Перед вызовом setState() после асинхронной операции всегда проверяйте mounted:
if (mounted) {
setState(() => _data = data);
}
Если виджет был удалён из дерева во время выполнения асинхронной операции, mounted станет false, и setState не вызовется. Это предотвращает исключение и утечки ресурсов.
setState() — удобный, но потенциально дорогой механизм, если использовать его бездумно. Каждый вызов setState перестраивает весь виджет и всех его потомков (если они не const). В глубоких деревьях или при частых вызовах это может приводить к просадкам FPS.
Основные стратегии оптимизации: минимизировать область перестроения (выносить изменяемые части UI в отдельные StatefulWidget), использовать const для неизменяемых потомков и избегать вызова setState в родительских виджетах, если изменилась только небольшая UX-деталь. Если состояние обновляется с высокой частотой (анимация, поток данных), рассмотрите AnimatedBuilder или ValueListenableBuilder.
По данным Flutter Performance Best Practices (Flutter.dev, февраль 2026), профилирование реальных приложений показывает, что до 40% всех вызовов setState можно заменить на const-дочерние виджеты или на реактивные билдеры (StreamBuilder, FutureBuilder). Это снижает среднее время сборки кадра на 15–25%.
| Сценарий | Альтернатива | Преимущество |
|---|---|---|
| Анимация | AnimatedBuilder | Перестраивает только анимируемый виджет |
| Поток данных | StreamBuilder | Реагирует на каждый элемент потока |
| Будущий результат | FutureBuilder | Управляет состояниями загрузки/ошибки |
| Локальное значение | ValueListenableBuilder | Реагирует на изменения одного значения |
Несмотря на универсальность setState(), в крупных проектах его применяют в основном для локального состояния. Для глобального или разделяемого состояния используются специализированные решения, каждое из которых заменяет или оборачивает setState.
Provider использует ChangeNotifier + notifyListeners как аналог setState, но с возможностью подписки нескольких виджетов. Bloc использует Streams — состояние изменяется через добавление событий в StreamController. Riverpod комбинирует подходы, предоставляя как локальное (StateProvider), так и асинхронное (AsyncNotifier) управление без привязки к StatefulWidget. Все три подхода избавляют от необходимости вручную вызывать setState — обновление UI происходит автоматически при изменении данных.
По данным Flutter Community Survey 2025 (Flutter Foundation, декабрь 2025), 74% разработчиков используют хотя бы один инструмент управления состоянием помимо setState. При этом 92% продолжают использовать setState для локальных данных текст-филда, чекбокса или простого счётчика — это считается best practice.
Первая и самая опасная ошибка — вызов setState после dispose. Асинхронная операция стартовала в initState, пользователь ушёл с экрана, виджет удалён, а колбэк асинхронной операции вызывает setState — приложение падает с исключением. Решение — всегда проверять mounted перед вызовом.
Вторая ошибка — вызов setState внутри build. Это приводит к бесконечному циклу: build → setState → dirty → build → setState → ... Flutter не блокирует такой вызов (вы получите StackOverflowError). setState может вызываться только в ответ на событие (нажатие кнопки, завершение Future, получение данных из потока).
Третья ошибка — изменение полей State без вызова setState. Разработчик пишет _count++ и ожидает, что UI обновится. Flutter не умеет отслеживать изменения полей автоматически — ему нужен явный сигнал через setState. Это фундаментальное отличие от реактивных фреймворков вроде Vue.js, где изменение данных автоматически триггерит обновление.
Четвёртая ошибка — вызов setState с асинхронным колбэком (async-лямбда). Как описано в разделе об асинхронности, изменения после await не будут захвачены, что приводит к багам, которые трудно воспроизвести. Используйте синхронный колбэк и вызывайте setState после await.
mounted в асинхронных колбэкахЧасто задаваемые вопросы
setState() уведомляет Flutter, что внутренние данные StatefulWidget изменились и UI нужно перестроить. Метод принимает колбэк, выполняет его синхронно, помечает виджет как dirty и планирует вызов build в следующем кадре.
UI не обновится. Flutter не отслеживает изменения полей автоматически. Значение поля изменится в памяти, но виджет останется в прежнем состоянии до следующего принудительного перестроения родителем.
Нельзя. Это приведёт к бесконечному циклу: build вызывает setState, который помечает виджет как dirty и снова вызывает build. Flutter не блокирует такую ситуацию — приложение упадёт с StackOverflowError.
Build выполнится один раз. Flutter собирает все dirty-элементы и перестраивает их пакетно в конце кадра. Второй setState до обработки первого просто добавляет элемент в тот же список dirty-элементов — повторного build не будет.
mounted — булев флаг, показывающий, что виджет всё ещё находится в дереве. Если после асинхронной операции вызвать setState без проверки mounted, а виджет уже удалён — приложение упадёт с исключением «setState called after dispose».
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также