> Как устроен жизненный цикл виджетов во Flutter (Flutter)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Верме

Стек: Flutter

> Пример ответа

Короткий ответ

Жизненный цикл виджета во Flutter - это последовательность методов, которые вызываются фреймворком от создания до уничтожения элемента. Ключевое различие - между StatefulWidget и его State: виджет пересоздаётся при каждом rebuild, а State сохраняется. Основные фазы: создание (initState), обновление (didChangeDependencies, didUpdateWidget), пересборка (build) и уничтожение (dispose). Понимание этих фаз критично для управления ресурсами, подписками и оптимизации производительности.

Подробное объяснение

Жизненный цикл во Flutter относится не к самому Widget (который иммутабелен и лёгок), а к связанному с ним Element и State. Element - это долгоживущий объект в дереве, который управляет связью между конфигурацией (Widget) и состоянием (State).

Основные фазы жизненного цикла State:

  1. initState() - вызывается один раз при вставке State в дерево. Здесь инициализируют контроллеры, подписки на Stream/ChangeNotifier, таймеры. Нельзя вызывать BuildContext.dependOnInheritedWidgetOfExactType - дерево ещё не готово.

  2. didChangeDependencies() - вызывается сразу после initState и затем при каждом изменении InheritedWidget, от которого зависит State. Здесь безопасно обращаться к MediaQuery, Theme, Provider. Часто используется для отложенной инициализации, зависящей от контекста.

  3. build() - вызывается при каждой пересборке: после initState, didChangeDependencies, didUpdateWidget, а также при вызове setState. Должен быть чистым и быстрым, без побочных эффектов.

  4. didUpdateWidget(covariant Widget oldWidget) - вызывается, когда родитель пересоздаёт виджет с новыми параметрами, но State сохраняется. Здесь сравнивают старые и новые свойства, обновляют подписки или внутренние данные.

  5. setState() - не метод жизненного цикла, но триггер для повторного вызова build. Помечает State как "грязный" и планирует пересборку.

  6. deactivate() - вызывается при удалении State из дерева (например, при перемещении в другое место дерева). Временное состояние - элемент ещё может быть повторно вставлен.

  7. dispose() - финальная фаза. Освобождают ресурсы: отписываются от стримов, закрывают контроллеры, уничтожают таймеры. После dispose использовать State нельзя.

Важно: Widget пересоздаётся при каждом rebuild родителя, но State и Element сохраняются, если тип и ключ виджета не изменились. Это основа оптимизации Flutter.

На практике

  • initState: создавайте TextEditingController, ScrollController, AnimationController, подписывайтесь на Stream или ChangeNotifier. Не делайте здесь тяжёлых вычислений - лучше в didChangeDependencies или после первого build.

  • didChangeDependencies: используйте для чтения Theme.of(context), MediaQuery.of(context), Provider.of<T>(context). Если данные из InheritedWidget нужны для инициализации - делайте это здесь, а не в initState.

  • didUpdateWidget: сравнивайте oldWidget.someField с widget.someField. Если изменился параметр, от которого зависит подписка - пересоздайте её.

  • dispose: обязательно отписывайтесь от всех подписок, закрывайте контроллеры. Иначе - утечки памяти и ошибки "setState called after dispose".

  • deactivate: редкий случай - используйте только если нужно знать о временном удалении из дерева (например, для анимаций при перемещении). Не полагайтесь на него для освобождения ресурсов.

  • build: не вызывайте setState внутри build - это приведёт к бесконечному циклу. Не делайте сетевых запросов и тяжёлых операций.

  • Ключи: если виджет меняет позицию в списке, используйте Key (например, ValueKey), чтобы State сохранялся корректно.

Пример кода

class CounterWidget extends StatefulWidget {
  final int initialValue;
  const CounterWidget({super.key, required this.initialValue});

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  late int _counter;
  late final TextEditingController _controller;
  late final StreamSubscription<int> _subscription;

  @override
  void initState() {
    super.initState();
    _counter = widget.initialValue;
    _controller = TextEditingController();
    _subscription = someStream.listen((value) {
      setState(() => _counter = value);
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final theme = Theme.of(context);
    // используем theme для инициализации, если нужно
  }

  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.initialValue != widget.initialValue) {
      setState(() => _counter = widget.initialValue);
    }
  }

  @override
  void dispose() {
    _subscription.cancel();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('$_counter'),
        TextField(controller: _controller),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

Как отвечать на собеседовании

Начните с разграничения Widget, Element и State - это показывает глубину понимания. Затем перечислите основные методы в порядке вызова: initStatedidChangeDependenciesbuild → (при обновлении) didUpdateWidgetbuilddispose. Подчеркните, что setState - не метод жизненного цикла, а триггер.

Приведите пример из практики: почему подписку создают в initState, а не в build, и почему обязательно отписываются в dispose. Упомяните didChangeDependencies для работы с InheritedWidget. Если спросят про deactivate - объясните редкий сценарий.

Хорошо добавить про оптимизацию: почему Widget пересоздаётся, а State нет, и как это связано с const конструкторами и Key.

Что проверяет интервьюер

  • Понимание разницы между Widget, Element и State.
  • Знание порядка вызова методов и их назначения.
  • Осознание последствий неправильного управления ресурсами (утечки, ошибки после dispose).
  • Умение применять didUpdateWidget для реактивного обновления.
  • Понимание роли InheritedWidget и didChangeDependencies.
  • Практический опыт: когда что использовать, а что не стоит.

Типичные ошибки

  • Путаница между initState и didChangeDependencies - первая инициализация контекстно-зависимых данных.
  • Вызов setState после dispose - приводит к исключению.
  • Создание тяжёлых объектов в build - убивает производительность.
  • Забытая отписка от стримов или таймеров в dispose.
  • Использование BuildContext после dispose (например, в асинхронных колбэках).
  • Непонимание, что didUpdateWidget вызывается только при изменении конфигурации, а не при каждом setState.
  • Попытка вызвать setState внутри build - бесконечный цикл пересборки.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью