Урок по треку Flutter и Dart 6 мин

Жизненный цикл State и приложения

createState, initState, didUpdateWidget, dispose и жизненный цикл приложения.

У StatelessWidget жизненного цикла почти нет: создали и вызвали build. У State он полноценный, и половина багов в Flutter-проектах это код не в том методе.

Методы State по порядку

  1. createState (у виджета): фреймворк создаёт объект State при вставке элемента в дерево.
  2. initState: один раз. Здесь создают контроллеры (TextEditingController, AnimationController), подписки, запускают начальную загрузку. Обязательно super.initState() первой строкой. Через context здесь нельзя подписываться на InheritedWidget: Theme.of(context) в initState бросит ошибку.
  3. didChangeDependencies: сразу после initState и потом каждый раз, когда изменился InheritedWidget, от которого зависит State (тема, локаль, MediaQuery, провайдер). Тут безопасно читать зависимости через context.
  4. build: может вызываться много раз в секунду. Должен быть чистым и быстрым: никаких запросов и подписок.
  5. didUpdateWidget(oldWidget): родитель перестроился и передал новый виджет того же типа и ключа. State сохранился, а параметры могли поменяться. Если зависели от widget.userId, сравнивают со старым и перезапрашивают. После него всегда вызывается build.
  6. setState: помечает элемент грязным, build будет на следующем кадре.
  7. deactivate: элемент убран из дерева. Может вернуться (перенос через GlobalKey) до конца кадра, тогда будет activate.
  8. dispose: окончательно. Освободить всё, что создали: controller.dispose(), subscription.cancel(), removeListener. super.dispose() последней строкой.
class _ProfileState extends State<Profile> {
  late final TextEditingController _name;
  StreamSubscription<User>? _sub;

  @override
  void initState() {
    super.initState();
    _name = TextEditingController();
    _sub = widget.repo.watch(widget.id).listen(_onUser);
  }

  @override
  void didUpdateWidget(Profile old) {
    super.didUpdateWidget(old);
    if (old.id != widget.id) {
      _sub?.cancel();
      _sub = widget.repo.watch(widget.id).listen(_onUser);
    }
  }

  void _onUser(User u) {
    if (!mounted) return;
    setState(() => _name.text = u.name);
  }

  @override
  void dispose() {
    _sub?.cancel();
    _name.dispose();
    super.dispose();
  }
  // build ...
}

mounted и setState после dispose

Асинхронный ответ может прийти, когда пользователь уже ушёл с экрана. setState после dispose бросает ошибку «setState() called after dispose()». Проверка if (!mounted) return; после каждого await в State и отмена подписок в dispose решают проблему.

Жизненный цикл приложения

AppLifecycleState сообщает, что делает всё приложение:

  • resumed: на экране и получает ввод;
  • inactive: видно, но без ввода (входящий звонок, переключатель приложений на iOS);
  • hidden: не видно (появилось в Flutter 3.13);
  • paused: в фоне;
  • detached: движок работает без окна.

Подписка: миксин WidgetsBindingObserver с методом didChangeAppLifecycleState или класс AppLifecycleListener с колбэками onResume, onPause и другими. Типичные задачи: поставить видео на паузу, сохранить черновик, переподключить сокет при возврате.

Частые ошибки

  • Theme.of(context) в initState.
  • Запрос данных в build: он уйдёт на каждом перестроении.
  • Забытый dispose у контроллера или подписки.
  • Игнорировать didUpdateWidget: экран показывает данные старого id.

Что спросят на собесе

  • Порядок методов State от создания до удаления.
  • Зачем didChangeDependencies и почему нельзя читать InheritedWidget в initState.
  • Когда вызывается didUpdateWidget и что в нём делать.
  • Что такое mounted, откуда ошибка «setState after dispose».
  • Состояния AppLifecycleState и как на них подписаться.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.