setState() — суть, механізм роботи та застосування

Автор: IT Sectr Опубліковано: 2026-07-01 Час читання: 9 хв

setState() — ключовий метод State у Flutter, який сповіщає фреймворк про зміну даних і запускає перебудову інтерфейсу. За даними офіційної документації Flutter (Flutter.dev, 2026), setState є основним механізмом реактивності в StatefulWidget: без його виклику UI не дізнається про зміни полів State і залишиться в попередньому стані. Метод приймає колбек VoidCallback, всередині якого розробник модифікує змінювані поля, після чого Flutter автоматично викликає build для перебудови віджета.

Головне

  • setState() — метод State, що маркує віджет як брудний (dirty) і планує перебудову UI в наступному кадрі
  • Колбек — setState приймає VoidCallback, всередині якого мають бути всі зміни полів State, що впливають на інтерфейс
  • Асинхронність — setTimeout або Future всередині setState не гарантують синхронність; мутації після await мають бути всередині іншого setState
  • Продуктивність — кожен виклик setState перебудовує весь віджет; для мінімізації використовуйте const дочірні віджети
  • mounted — перед викликом setState в асинхронних колбеках обов'язково перевіряйте mounted, інакше — виняток

Що таке setState()?

setState() — вбудований метод класу State у Flutter, призначений для сповіщення фреймворку про те, що внутрішній стан віджета змінився і необхідно перебудувати UI. Без виклику setState Flutter не знає про зміни — навіть якщо поля State були модифіковані, інтерфейс залишиться незмінним до наступної примусової перебудови батьком.

Сигнатура методу: void setState(VoidCallback fn). Колбек виконується синхронно всередині setState, і тільки після його завершення State позначається як брудний. Це гарантує, що всі зміни застосовуються атомарно до перебудови. За даними Специфікації мови Dart (Dart Team, 2026), атомарність setState запобігає стану гонки, при якому build міг би побачити частково оновлений стан.

setState не приймає аргументів, не повертає значення і не може бути перевизначений. Це фінальний (запечатаний) метод класу State. Розробник не може змінити його поведінку — тільки використовувати за призначенням. Спроба викликати setState поза State (наприклад, з іншого класу) неможлива, оскільки метод оголошений у класі State.

setState не змінює стан — змінюєте ви

Поширена помилка — вважати, що setState сам змінює стан. Це не так. setState лише викликає переданий колбек (у якому розробник змінює поля) і потім сигналізує фреймворку про необхідність build. Колбек обов'язковий — передача null або порожнього колбека призведе до помилки.

Як працює setState()?

Механізм роботи setState() можна розбити на чотири етапи. Перший — виклик методу з колбеком. Другий — синхронне виконання колбека, всередині якого змінюються поля State. Третій — State позначається як брудний у спеціальному полі _dirty. Четвертий — наприкінці поточного мікрозавдання Flutter обходить усі брудні елементи та викликає їх build у порядку появи в дереві.

Важлива деталь: setState не викликає build негайно. Flutter використовує стратегію пакетного оновлення: всі брудні елементи збираються та перебудовуються за один кадр. Це означає, що якщо setState викликано кілька разів у рамках одного синхронного блоку, build виконається тільки один раз — після завершення всіх змін. Така оптимізація запобігає множинним перебудовам за кадр.

За даними Flutter Engine Team (Google, 2025), механізм брудних прапорців заснований на проході BuildOwner._dirtyElements. Кожен брудний StatefulElement додається до списку та обробляється на етапі оновлення кадру. Якщо віджет було видалено з дерева до обробки, він автоматично виключається зі списку брудних елементів.

Гарантії setState

  • Колбек виконується синхронно до маркування брудним
  • build викликається не більше одного разу за кадр (навіть при множинних викликах setState)
  • Оновлення UI відбувається на наступному кадрі (зазвичай ~16ms при 60 FPS)
  • Після dispose виклик setState заборонений — викидається виняток
  • Під час build виклик setState заборонений — нескінченний цикл

Приклади коду на Dart

Базовий приклад setState() з інкрементом лічильника. Демонструє коректне використання: зміна поля всередині колбека:

dart
class _CounterState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++; // мутація поля всередині колбека
    });
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _increment,
      child: Text('$_count'),
    );
  }
}

Приклад із текстовим полем і контролером — setState() для керування видимістю пароля:

dart
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

Якщо потрібно змінити кілька полів, всі зміни виконуються всередині одного setState. Це гарантує, що build побачить консистентний стан:

dart
setState(() {
  _isLoading = false;
  _items = newItems;
  _error = null;
});

Три поля змінюються в одному колбеку — build виконається один раз і побачить всі зміни одночасно. Якби кожен виклик був окремим setState, build все одно виконався б одноразово завдяки пакетній обробці брудних елементів.

Асинхронність і setState

Один із найважливіших нюансів setState() — його поведінка з асинхронними операціями. Колбек setState виконується синхронно, але якщо всередині нього викликати await, код після await виконається вже після того, як setState завершить роботу. Це означає, що зміни полів після await не будуть захоплені поточним setState.

Правильний підхід: асинхронна операція виконується ззовні setState, а setState викликається після її завершення. Весь код між отриманням результату та викликом setState виконується в синхронному контексті після await:

dart
// ПРАВИЛЬНО: await поза setState
Future<void> _loadData() async {
  final result = await ApiService.fetchData();
  setState(() {
    _data = result;
    _isLoading = false;
  });
}

// НЕПРАВИЛЬНО: await всередині setState — без гарантії оновлення
void _loadDataWrong() {
  setState(() async {
    _data = await ApiService.fetchData(); // setState повертається до завершення await
    _isLoading = false; // цей код не захоплюється setState
  });
}

За даними Flutter docs (Dart async patterns, 2026), передача асинхронного колбека в setState є антипатерном, оскільки setState очікує VoidCallback (синхронну функцію), а асинхронна функція повертає Future, який ігнорується. Зміни після першого await у такому колбеку не будуть коректно оброблені фреймворком.

Перевірка mounted в асинхронних сценаріях

Перед викликом setState() після асинхронної операції завжди перевіряйте mounted:

dart
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%.

Коли setState надлишковий

СценарійАльтернативаПеревага
АнімаціяAnimatedBuilderПеребудовує тільки анімований віджет
Потік данихStreamBuilderРеагує на кожен елемент потоку
Майбутній результатFutureBuilderКерує станами завантаження/помилки
Локальне значенняValueListenableBuilderРеагує на зміни одного значення

Альтернативи setState

Незважаючи на універсальність 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

  • Стан використовується тільки одним віджетом
  • Просте булеве або числове значення (фокус, видимість, лічильник)
  • Прототипування та швидкі експерименти
  • Контролери (TextEditingController, PageController) все одно вимагають StatefulWidget

Типові помилки

Перша і найнебезпечніша помилка — виклик setState після dispose. Асинхронна операція стартувала в initState, користувач пішов з екрана, віджет видалено, а колбек асинхронної операції викликає setState — додаток падає з винятком. Рішення — завжди перевіряти mounted перед викликом.

Друга помилка — виклик setState всередині build. Це призводить до нескінченного циклу: build → setState → брудний → build → setState → ... Flutter не блокує такий виклик (ви отримаєте StackOverflowError). setState може викликатися тільки у відповідь на подію (натискання кнопки, завершення Future, отримання даних із потоку).

Третя помилка — зміна полів State без виклику setState. Розробник пише _count++ і очікує, що UI оновиться. Flutter не вміє відстежувати зміни полів автоматично — йому потрібен явний сигнал через setState. Це фундаментальна відмінність від реактивних фреймворків на кшталт Vue.js, де зміна даних автоматично тригерить оновлення.

Четверта помилка — виклик setState з асинхронним колбеком (async-лямбда). Як описано в розділі про асинхронність, зміни після await не будуть захоплені, що призводить до багів, які важко відтворити. Використовуйте синхронний колбек і викликайте setState після await.

Пам'ятка щодо безпечного setState

  • Завжди перевіряй mounted в асинхронних колбеках
  • Не викликай setState всередині build
  • Не передавай async-лямбду в setState
  • Не змінюй поля State поза setState
  • Якщо змінюєш кілька полів — роби це в одному setState

Поширені запитання

Що робить setState() у Flutter?

setState() сповіщає Flutter, що внутрішні дані StatefulWidget змінилися і UI потрібно перебудувати. Метод приймає колбек, виконує його синхронно, позначає віджет як брудний і планує виклик build у наступному кадрі.

Що буде, якщо не викликати setState після зміни поля?

UI не оновиться. Flutter не відстежує зміни полів автоматично. Значення поля зміниться в пам'яті, але віджет залишиться в попередньому стані до наступної примусової перебудови батьком.

Чи можна викликати setState всередині build?

Не можна. Це призведе до нескінченного циклу: build викликає setState, який позначає віджет як брудний і знову викликає build. Flutter не блокує таку ситуацію — додаток упаде з StackOverflowError.

Скільки разів виконається build при двох послідовних викликах setState?

Build виконається один раз. Flutter збирає всі брудні елементи та перебудовує їх пакетно наприкінці кадру. Другий setState до обробки просто додає елемент у той самий список брудних елементів — повторної перебудови не буде.

Що таке mounted і чому він важливий для setState?

mounted — булевий прапорець, що показує, чи віджет все ще знаходиться в дереві. Якщо після асинхронної операції викликати setState без перевірки mounted, а віджет уже видалено — додаток упаде з винятком «setState called after dispose».

Підсумки

  • setState() — метод State, що сповіщає Flutter про зміну даних і запускає перебудову UI в наступному кадрі
  • Механізм роботи — синхронне виконання колбека, маркування State як брудного, пакетна перебудова всіх брудних елементів наприкінці кадру
  • Асинхронність — async-колбеки в setState не працюють; await має бути ззовні, а setState — після отримання результату
  • mounted — обов'язкова перевірка перед setState в асинхронних операціях для запобігання винятку
  • Оптимізація — мінімізуйте область перебудови через const дочірні віджети та виносьте анімації в AnimatedBuilder
  • Альтернативи — для глобального стану використовуйте Riverpod, Bloc або Provider; setState залишайте для локальних даних
  • Правило — не викликайте setState всередині build, не передавайте async-лямбди, завжди перевіряйте mounted

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також