⚙️

【Flutter】StatefulWidgetのライフサイクルを完全に理解する

に公開

はじめに

Flutterの開発をしていると、initStateやdispose といったライフサイクルメソッドを使う機会は多いですが、「どの順番で呼ばれるんだっけ?」や「この処理はどこで書くのが正しいんだろう?」と迷うことが時々あります。

自分自身も開発の中で何度か確認することがあり、毎回調べ直すよりも一度整理しておいた方が良いと思い、この記事をまとめました。
備忘を兼ねつつ、後から見返したときにも理解しやすいように、各メソッドの呼ばれるタイミングや使いどころを一覧にしています。

よく使うinitStateやdisposeに加えて、didChangeDependenciesやdidUpdateWidgetなどのやや馴染みの薄いメソッドも含めて整理しています。

StatefulWidgetのライフサイクルメソッド一覧

StatefulWidgetのStateは、画面の生成 → 更新 → 破棄という「一生」を持っています。
その過程でFlutterは以下のメソッドを順番に呼び出します。

StatefulWidgetのライフサイクル(呼び出し順)

初期化フェーズ

メソッド 呼ばれるタイミング よくある用途 ポイント
createState StatefulWidgetが生成された直後 Stateインスタンスを紐付ける ほぼ触ることはない
initState Stateが作られた直後(1回だけ) 初期化、API読み込み、Controller生成 super.initState()を最初に呼ぶ
didChangeDependencies initStateの直後 / InheritedWidgetが変わった時 contextに依存する初期処理 初回も呼ばれるため、初期処理がここに入ることがある
didUpdateWidget 親Widgetから渡された値が更新された時 前の値 → 新しい値を引き継ぐ処理 アニメーション再連結などに使う

描画・更新フェーズ

メソッド 呼ばれるタイミング よくある用途 ポイント
build UIを描画する時 Widgetツリーを返す 副作用(API呼び出しなど)は書かない
setState 状態変更 → UI再描画を行いたい時 カウンター更新など 呼び出すとbuildが再実行される
didChangeAppLifecycleState アプリの前面/背面切り替え時 ビデオ通話・音声処理の一時停止 WidgetsBindingObserverのmixinが必要

画面遷移・再配置フェーズ

メソッド 呼ばれるタイミング よくある用途 ポイント
deactivate Stateがツリーから一時的に外れる時 デバッグログ程度 あまり使わないが「存在は知っておく」
activate 再びツリーに戻る時 特殊ケースでのみ使用 知識として押さえる程度
reassemble Hot Reload時のみ デバッグ用 本番アプリでは呼ばれない

破棄フェーズ

メソッド 呼ばれるタイミング よくある用途 ポイント
dispose Stateが破棄される時(1回だけ) Controller, Stream, Timerの解放 ここを忘れるとメモリリークする

実行順を確認するデモコード

以下のサンプルでは、ライフサイクルメソッドにdebugPrint()を書き、実際に画面を表示したときに『どの順番で呼ばれるか』がわかるようにしています。

life_cycle_demo_page.dart
import 'package:flutter/material.dart';

class LifecycleDemoPage extends StatefulWidget {
  const LifecycleDemoPage({super.key});

  @override
  State<LifecycleDemoPage> createState() => _LifecycleDemoPageState();
}

class _LifecycleDemoPageState extends State<LifecycleDemoPage> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    debugPrint('initState');
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    debugPrint('didChangeDependencies');
  }

  @override
  void didUpdateWidget(covariant LifecycleDemoPage oldWidget) {
    super.didUpdateWidget(oldWidget);
    debugPrint('didUpdateWidget');
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    debugPrint('didChangeAppLifecycleState: $state');
  }

  @override
  Widget build(BuildContext context) {
    debugPrint('build');
    return Scaffold(
      appBar: AppBar(title: const Text('Lifecycle Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              child: const Text('setStateしてみる'),
              onPressed: () {
                setState(() {
                  debugPrint('setState → buildが再実行される');
                });
              },
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              child: const Text('popしてみる(別画面に移動)'),
              onPressed: () {
                debugPrint('pop → deactivate/disposeが実行される');
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
    );
  }

  @override
  void deactivate() {
    super.deactivate();
    debugPrint('deactivate');
  }

  @override
  void dispose() {
    debugPrint('dispose');
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}

実行ログの例

アプリ起動 → 画面表示
flutter: initState
flutter: didChangeDependencies
flutter: build
setStateしたとき
flutter: setState build が再実行される
flutter: build
popしたとき(画面遷移で別画面に移動したとき等)
flutter: pop deactivate/disposeが実行される
flutter: deactivate
flutter: dispose

Tips:ライフサイクルメソッド活用のコツと落とし穴

1.initStateの注意点

よく使うケース:
・Controller、Animation、Timer、Streamなどの初期化
・ 初回のみ実行したい処理の登録(例:リスナー登録)

注意点:
・initStateではまだBuildContextが安定していないため、contextを直接使うのは避ける。

@override
void initState() {
  super.initState();
  // NG: contextを使ってNavigatorやProviderにアクセス
  // Navigator.of(context).push(...); ← エラーの原因
}

・Futureを使いたい場合は「遅延呼び出し」で回避する。

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    // 画面描画後に実行
    _loadData();
  });
}

2.didChangeDependenciesの使いどころ

主な用途:
・InheritedWidgetやProviderなど、上位ウィジェットに依存するデータを参照したいとき。
・依存が変更された時(例:Locale、Theme、MediaQuery)が再評価される。

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  final theme = Theme.of(context);
  print('テーマが変わりました: ${theme.brightness}');
}

補足:
・initStateの直後にも1回呼ばれる(=初期処理に使える)
・重い処理を入れると、依存変化時に毎回走るので注意

3.disposeの重要性

主な用途:
・Controller、Stream、FocusNodeなどのリソース解放
 →FlutterはGCがあるが、リスナー登録などの明示的解放は必要。
・メモリリーク防止の要。

late final TextEditingController _controller;

@override
void initState() {
  super.initState();
  _controller = TextEditingController();
}

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

よくあるミス:
・super.dispose()を最初に呼んでしまう(順番逆)
 →正しくは最後に呼ぶ!

4.その他の実践Tips

ケース 推奨メソッド 理由
画面初回ロードでデータ取得 initState + addPostFrameCallback ビルド後に安全に非同期呼び出し
Theme/Locale変更に応じた再構築 didChangeDependencies 上位依存の変更を検知
ストリーム購読解除 dispose リーク防止
Widget更新検知(親パラメータ変更) didUpdateWidget oldWidgetと比較可能

5.よくある勘違いポイント

勘違い 正しい理解
initState内でawaitできる 🆖 build前にawaitは禁止。addPostFrameCallbackを使う
disposeを省略してもGCが解放する 🆖 StreamやControllerは明示的にdispose必要
didChangeDependenciesはほとんど使わない 🆖 Provider使用時には非常に重要
deactivateは不要 🆗 テスト・デバッグやNavigator戻り時に役立つこともあるかも

おわりに

Flutterのライフサイクルメソッドは、どれも一度は耳にするものですが、それぞれが呼ばれるタイミングや役割をしっかり把握しておくと、状態管理やリソース解放の精度がぐっと上がります。

今回あらためて整理してみると、「普段なんとなく使っていた箇所」にも明確な意図や流れがあることに気づきました。
開発中に迷ったときや、「この処理、どこに書くのが正解かな?」と思ったときに、この記事を見返して整理できるようにしておきたいと思います。

もし同じようにライフサイクルの流れを確認したい方がいれば、少しでも参考になれば幸いです。

Discussion