【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()を書き、実際に画面を表示したときに『どの順番で呼ばれるか』がわかるようにしています。
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
flutter: setState → build が再実行される
flutter: build
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