StatefulWidget — віджет Flutter зі змінним станом, що дозволяє UI реагувати на дії користувача, асинхронні події та потоки даних. Згідно з офіційною документацією Flutter (Flutter.dev, 2026), StatefulWidget використовується для всіх інтерактивних елементів застосунку: форм введення, анімацій, прапорців, перемикачів та екранів, що завантажують дані з мережі. На відміну від StatelessWidget, він створює окремий об'єкт State, який зберігається протягом усього життєвого циклу і може бути перебудований без перестворення самого віджета.
Головне
StatefulWidget — це клас Flutter, який може змінювати свій стан у відповідь на дії користувача, системні події або асинхронні операції. На відміну від StatelessWidget, StatefulWidget не рендериться безпосередньо — він створює об'єкт State, який відповідає за рендеринг. Розділення на два класи (Widget і State) дозволяє Flutter перебудовувати UI без перестворення самого віджета, що дає значну перевагу в продуктивності під час частих оновлень.
Архітектура StatefulWidget слідує патерну «розділення змінного та незмінного»: сам віджет залишається незмінним (як StatelessWidget), а весь змінний стан зберігається в окремому об'єкті State. Це дозволяє Flutter перевикористовувати віджети, порівнюючи їх за типом і Key, і при цьому зберігати актуальний стан між перебудовами.
За даними Google (Flutter Architectural Overview, 2026), StatefulWidget оптимальний для сценаріїв, де стан змінюється більше одного разу за час життя віджета: текстові поля, анімації, таймери, потоки даних, асинхронні завантаження. Для одноразової ініціалізації достатньо StatelessWidget.
StatefulWidget обов'язковий, коли віджет повинен реагувати на зовнішні події: натискання кнопки, завершення HTTP-запиту, оновлення даних з бази, підписка на WebSocket. Він також необхідний для віджетів з анімацією, текстових полів з контролерами та компонентів, що керують фокусом. Якщо віджет лише відображає дані і не породжує подій — використовуйте StatelessWidget.
StatefulWidget складається з двох класів: власне StatefulWidget (легкий, незмінний) і State (важкий, змінний). Фреймворк створює State через метод createState(), який викликається одноразово при вбудовуванні в дерево. State отримує посилання на віджет через властивість widget і може звертатися до його полів у будь-який момент життєвого циклу.
Життєвий цикл StatefulWidget складається з шести основних етапів, кожен з яких надає перевизначуваний метод для виконання специфічних завдань. Розуміння цих етапів критично важливе для коректної роботи з ресурсами та уникнення витоків пам'яті.
createState — перший метод життєвого циклу, що викликається при вбудовуванні StatefulWidget у дерево. Він повинен повертати новий екземпляр State, асоційований з даним віджетом. Цей метод викликається рівно один раз за весь час життя елемента. Важливо не виконувати тут важких операцій — createState має бути максимально легким.
initState — викликається відразу після створення State, до першої побудови UI. Тут виконуються: ініціалізація контролерів (TextEditingController, AnimationController), підписка на потоки даних (StreamSubscription), налаштування таймерів та початкова ініціалізація полів. За даними Flutter docs (Flutter.dev, 2026), в initState не можна викликати BuildContext.of() — дерево ще не повністю змонтовано.
didChangeDependencies — викликається після initState і кожного разу, коли змінюються залежності InheritedWidget. Це відповідне місце для виклику MediaQuery.of(context) або підписки на Theme — значення, які можуть змінитися під час роботи застосунку. Якщо віджет використовує InheritedWidget, логіка ініціалізації повинна бути тут, а не в initState.
build — основний метод, що повертає дерево віджетів. Викликається після initState, після didChangeDependencies і після кожного setState. didUpdateWidget викликається, коли батько перебудовується і передає StatefulWidget з новими параметрами. Тут можна порівняти старі та нові поля віджета і, при необхідності, оновити стан.
dispose — завершальний етап життєвого циклу. Тут звільняються всі ресурси: відписуються від потоків, видаляються контролери, скасовуються таймери. Невиклик dispose призводить до витоків пам'яті. Після dispose State вважається мертвим — виклик setState всередині нього викидає виняток.
Механізм роботи StatefulWidget заснований на узгодженій роботі трьох сутностей: Widget (легкий опис), Element (проміжний шар) і State (сховище даних). Коли Flutter зустрічає StatefulWidget в описі, він створює StatefulElement, який викликає createState і зберігає посилання на об'єкт State. При перебудові батька Flutter порівнює новий віджет з поточним Element — якщо тип і Key збігаються, Element оновлюється, а State залишається незмінним.
Стан змінюється тільки через виклик setState, який повідомляє фреймворку про необхідність перебудови. Важливо розуміти: setState не змінює стан автоматично — він лише маркує віджет як «брудний». Розробник самостійно оновлює поля State у колбеку, переданому в setState. Після завершення колбека Flutter викликає build і оновлює UI.
За даними команди Dart/Flutter (Dart Language Specification, 2026), таке розділення гарантує, що всі зміни стану відбуваються синхронно до виклику build, виключаючи ситуацію, коли UI відображає частково оновлені дані. Це ключовий механізм консистентності інтерфейсу у Flutter.
Розглянемо простий StatefulWidget — лічильник натискань на кнопку. Він демонструє базовий патерн: створення State, ініціалізація поля в initState, зміна через setState:
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $_count'),
ElevatedButton(
onPressed: _increment,
child: const Text('Increment'),
),
],
);
}
}
Приклад з асинхронним завантаженням даних і управлінням життєвим циклом. StatefulWidget завантажує дані з мережі і відображає стан завантаження:
class UserProfilePage extends StatefulWidget {
final String userId;
const UserProfilePage({super.key, required this.userId});
@override
State<UserProfilePage> createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
UserModel? _user;
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadUser();
}
Future<void> _loadUser() async {
final user = await UserService.fetchUser(widget.userId);
setState(() {
_user = user;
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) return const CircularProgressIndicator();
return Text('Hello, ${_user!.name}');
}
}
У другому прикладі важливо зазначити: initState запускає асинхронну операцію, але сам метод не є асинхронним. Асинхронність реалізується через async/await всередині окремого методу _loadUser, який оновлює стан через setState після завершення запиту. Такий підхід гарантує, що віджет коректно відобразить індикатор завантаження до отримання даних.
Вибір між StatefulWidget і StatelessWidget — не лише питання наявності стану. StatefulWidget надає повноцінний життєвий цикл з методами initState, didChangeDependencies, didUpdateWidget і dispose, що необхідно для роботи з контролерами, анімаціями та потоками. StatelessWidget, у свою чергу, не має цих методів і завжди легший для фреймворку.
Рекомендація команди Flutter (Flutter docs, 2026) — мінімізувати кількість StatefulWidget у застосунку, піднімаючи стан вгору по дереву (State Hoisting) або використовуючи рішення для керування станом (Riverpod, Bloc, Provider). Кожен StatefulWidget створює об'єкт State, який живе до видалення елемента — чим більше таких віджетів, тим вище навантаження на пам'ять.
| Критерій | StatefulWidget | StatelessWidget |
|---|---|---|
| Стан | Змінний | Незмінний |
| Життєвий цикл | 6 етапів | Тільки build |
| Об'єкт State | Створюється окремо | Не потрібен |
| setState | Доступний | Недоступний |
| Підписки | initState/dispose | Не підтримуються |
| const конструктор | Обмежений | Повністю підтримується |
| Споживання пам'яті | Вище | Нижче |
StatefulWidget потребує більше ресурсів, ніж StatelessWidget, через необхідність створювати та підтримувати об'єкт State. Однак правильне використання StatefulWidget не призводить до проблем продуктивності, якщо дотримуватися кількох правил. По-перше, уникайте глибокої вкладеності StatefulWidget — кожен рівень додає накладні витрати на обхід дерева. По-друге, розбивайте складні StatefulWidget на кілька простих, кожен з яких відповідає за свою частину стану.
За даними дослідження Flutter Performance (Flutter.dev, лютий 2026), найчастішою причиною просідання FPS є виклик setState у батьківському віджеті, який перебудовує всіх нащадків, включаючи StatelessWidget, що не змінили своє відображення. Рішення — виносити змінну частину UI в окремий StatefulWidget, щоб setState перебудовував лише мінімум необхідних віджетів.
Використання const всередині State — ще один важливий прийом. Якщо дочірні віджети оголошені як const, Flutter не буде перебудовувати їх при виклику setState у батьку. Це знижує навантаження на фреймворк і зменшує час рендерингу кадру.
Кожен виклик setState запускає повну перебудову віджета. Якщо стан змінюється з високою частотою (наприклад, анімація або потік даних), розгляньте використання AnimatedBuilder, ValueListenableBuilder або StreamBuilder замість ручного виклику setState. Ці віджети оптимізують перебудову, оновлюючи лише ту частину UI, яка дійсно змінилася.
Перша типова помилка з StatefulWidget — виклик setState після dispose. Коли віджет видалено з дерева, State вважається мертвим, і будь-який виклик setState викидає виняток «setState called after dispose». Найчастіше це відбувається, коли асинхронна операція завершується після видалення віджета. Рішення — перевіряти прапорець mounted перед викликом setState або скасовувати асинхронні операції в dispose.
Друга помилка — виконання важких обчислень у методі build. Оскільки build викликається при кожному setState і при кожній перебудові батька, всі обчислення повинні бути максимально легкими. Якщо потрібно виконати ресурсоємну операцію — винесіть її в окремий ізолят (Isolate) або кешуйте результат у полі State.
Третя помилка — відсутність виклику super.initState() і super.dispose(). Перевизначаючи ці методи, розробник зобов'язаний викликати батьківську реалізацію. Якщо цього не зробити, фреймворк не зможе коректно керувати станом Element, що призведе до важковідстежуваних багів.
mounted перед setState в асинхронних колбекахsuper.initState() і super.dispose()Часті запитання
StatefulWidget може змінювати свій стан через setState, має життєвий цикл (initState, dispose) і створює окремий об'єкт State. StatelessWidget не може змінювати стан і не має методів життєвого циклу — він просто відображає передані дані.
createState викликається рівно один раз для кожного екземпляра StatefulElement. Навіть якщо батько перебудовується багаторазово, поки тип і Key віджета не змінюються, createState не викликається — використовується існуючий об'єкт State.
Ресурси не звільняться: контролери продовжать працювати у фоні, підписки на потоки залишаться активними, таймери не скасуються. Це призводить до витоків пам'яті і може викликати виклик setState після dispose, що викидає виняток.
Так, конструктор StatefulWidget може бути const. Однак це не дає такого ж виграшу, як для StatelessWidget — об'єкт State все одно буде створюватися при першому вбудовуванні. const впливає тільки на сам віджет (легку обгортку), не на State.
didUpdateWidget викликається, коли батько передає StatefulWidget з новими параметрами. Це потрібно, щоб синхронізувати стан з новими даними — наприклад, якщо змінився userId в параметрах, потрібно завантажити профіль нового користувача.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також