Всегда ли вызывается метод initState() у State объекта в 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() был вызван ранее.

Ответ 18+ 🔞

Э, слушай, вот объясняю про этот ваш initState(). Многие думают, что он вызывается всегда, как часы. А нихуя! Он вызывается ровно один раз, когда объект State впервые впихивают в дерево виджетов. И точка.

А вот когда initState() НЕ вызывается, ёпта:

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

Короче, типичный сценарий, как этим пользоваться:

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

  @override
  void initState() {
    super.initState(); // Это, бля, святое! Всегда первым делом!
    print('initState: Виджет встроен в дерево, ура!');

    // Сюда пихаем всё, что должно случиться один раз при рождении:
    // 1. Инициализация полей, которые от `widget` зависят.
    _cachedData = ComplexData(widget.initialParam);

    // 2. Подписки на всякие Stream и Listenable. Чтобы потом не орать "куда делись данные?!".
    _streamSubscription = myStream.listen(_handleData);
    _scrollController.addListener(_onScroll);

    // 3. Запуск асинхронных делишек (но сам метод `initState` не делай async!).
    WidgetsBinding.instance.addPostFrameCallback((_) {
      // Код, который выполнится после того, как первый кадр отрисуется.
    });
    _loadInitialData(); // А вот это уже async-метод, который дергаем отсюда.
  }

  Future<void> _loadInitialData() async {
    final data = await Api.fetchData();
    setState(() => _cachedData = data); // Вот тут обновляем состояние.
  }

  @override
  void dispose() {
    // Всё, что создал в `initState`, тут же и прибивай! Иначе утечки памяти будут — овердохуища.
    _streamSubscription.cancel();
    _scrollController.dispose();
    super.dispose();
  }

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

Железобетонная гарантия: Если виджет встроился в дерево и у него вызвался build(), будь уверен — initState() перед этим уже отработал. Иначе никак, чувак.