StatefulWidget и жизненный цикл
StatefulWidget умеет помнить и менять данные внутри себя, перерисовываясь через setState.
Суть: StatefulWidget хранит изменяемое состояние в отдельном объекте State. Вызов
setStateпомечает виджет «грязным», и Flutter перестраивает его. Жизненный цикл управляет созданием, обновлением и уничтожением.
Разделение StatefulWidget на две части — сам виджет и объект State — поначалу кажется лишней церемонией, но у него есть глубокая причина. Виджет, как и любой другой, неизменяем и может пересоздаваться; а вот State живёт дольше и сохраняется, пока виджет остаётся в дереве на своём месте. Именно поэтому изменяемые данные хранятся в State, а не в самом виджете: State переживает перестроения и помнит счётчик, текст поля или прогресс анимации.
Счётчик, переключатель, поле ввода, анимация — всё, что меняется в ответ на действия пользователя, требует памяти. StatelessWidget такой памяти не имеет. Поэтому существует StatefulWidget: он состоит из двух частей — неизменяемого виджета и связанного с ним объекта State, который и хранит меняющиеся данные.
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'),
);
}
}
Ключевой момент — setState. Вы меняете данные внутри переданной ему функции, и Flutter после этого заново вызывает build. Если поменять count без setState, значение изменится, но экран не обновится.
Как работает жизненный цикл под капотом
У State есть упорядоченные этапы жизни. createState создаёт объект состояния. initState вызывается один раз при рождении — здесь делают начальную настройку и запросы. build рисует интерфейс. setState запускает повторный build. didUpdateWidget срабатывает, когда родитель дал новые данные. dispose вызывается при удалении — здесь освобождают ресурсы (контроллеры, подписки).
createState()
|
v
initState() <- один раз: подписки, начальные данные
|
v
didChangeDependencies()
|
v
+-> build() <- рисуем интерфейс
| ^ |
| | v
| setState() <- меняем данные -> снова build
| |
| didUpdateWidget() <- родитель дал новые входные данные
| |
+----+
|
v
dispose() <- удаление: отписки, освобождение
# Модель жизненного цикла State и setState -> build
class CounterState:
def __init__(self):
self.count = 0
self.init_state()
def init_state(self):
print('initState: настройка один раз')
def set_state(self, mutator):
mutator() # меняем данные внутри
self.build() # затем перестраиваем
def build(self):
print(f'build: Нажато {self.count}')
def dispose(self):
print('dispose: освобождаем ресурсы')
s = CounterState()
s.build()
s.set_state(lambda: setattr(s, 'count', s.count + 1)) # +1 и rebuild
s.set_state(lambda: setattr(s, 'count', s.count + 1))
s.dispose()
Частые ошибки
- Менять состояние без
setState— данные обновятся, а экран нет. - Забыть
disposeдля контроллеров и подписок — утечки памяти. - Тяжёлая работа в
buildвместоinitState— запрос полетит при каждом перестроении.
Best practices
- Меняйте состояние только внутри функции, переданной в
setState. - Начальную настройку и подписки делайте в
initState, освобождение — вdispose. - Делайте StatefulWidget настолько маленьким, насколько возможно: перестраивается только он, не весь экран.
Метод didUpdateWidget заслуживает внимания, потому что новички про него забывают. Он вызывается, когда родитель передал виджету новые входные данные, и позволяет среагировать на это — например, перезапустить анимацию или загрузку, если изменился переданный идентификатор. Без него State может застрять на старых данных, показывая профиль одного пользователя, когда родитель уже переключился на другого. Помните: входные данные виджета могут меняться, и State обязан это учитывать.
Итог: StatefulWidget — это виджет с памятью. Запомните цикл initState → build → setState↔build → dispose и золотое правило: меняешь данные — оборачивай в setState. Подробнее о потоке перестроения — в разделе про состояние.