Всегда ли вызывается метод initState() у State объекта в Flutter?

«Всегда ли вызывается метод initState() у State объекта в Flutter?» — вопрос из категории Жизненный цикл, который задают на 29% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Нет, initState() вызывается не всегда. Это метод жизненного цикла, который вызывается ровно один раз при первом встраивании (insert) объекта State в дерево виджетов.

Когда initState() НЕ вызывается:

  1. Если объект State был создан, но соответствующий StatefulWidget никогда не был добавлен в дерево (например, из-за условного оператора if (false) MyWidget()).
  2. При горячей перезагрузке (Hot Reload) состояние сохраняется, и initState() не вызывается заново.
  3. Если виджет был удален из дерева и затем снова добавлен с тем же ключом (key), Flutter может переиспользовать существующий объект State, и initState() вызван не будет.

Типичный сценарий использования в Flutter:

class _MyHomePageState extends State<MyHomePage> {
  late StreamSubscription<int> _streamSubscription;
  final _scrollController = ScrollController();
  ComplexData? _cachedData;

  @override
  void initState() {
    super.initState(); // Всегда вызывайте super.initState() первым!
    print('initState: Виджет встроен в дерево.');

    // Идеальное место для:
    // 1. Инициализации final/ late final полей, зависящих от widget.
    _cachedData = ComplexData(widget.initialParam);

    // 2. Подписки на Stream или Listenable.
    _streamSubscription = myStream.listen(_handleData);
    _scrollController.addListener(_onScroll);

    // 3. Запуска одноразовых асинхронных операций (но не async/await).
    WidgetsBinding.instance.addPostFrameCallback((_) {
      // Код, который должен выполниться после первого построения кадра.
    });
    _loadInitialData(); // Метод, который запускает Future.
  }

  Future<void> _loadInitialData() async {
    final data = await Api.fetchData();
    setState(() => _cachedData = data);
  }

  @override
  void dispose() {
    // Все, что создано в initState, должно быть очищено в dispose.
    _streamSubscription.cancel();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        controller: _scrollController,
        children: [
          Text('Данные: ${_cachedData?.value}'),
        ],
      ),
    );
  }
}

Гарантия: Если виджет был вставлен в дерево и у него вызвался build(), то initState() был вызван ранее.