StatefulWidget の内部構造を理解するための基礎知識
Riverpod や Flutter Hooks は、Flutter の状態管理をより扱いやすくするための仕組みですが、その土台には必ず StatefulWidget の仕組みがあります。これらは「まったく新しい概念」ではなく、StatefulWidget・State・Element・Widget ツリーの上に成り立つラッパーや抽象化だと捉えると理解しやすくなります。
そのため、Riverpod や Hooks を本格的に使い始める前に、まずは StatefulWidget が内部でどう動いているのか を押さえておくことが、とても大きな意味を持ちます。
ここでは、Flutter の UI がどのように構成され、State・Element・Widget ツリー・setState がどう連携しているのかを、4つの登場人物という形で整理していきます。
Flutter の UI は、次の4つの役者が協力して動いています。
- StatefulWidget(設計図)
- Element(UI の実体・司令塔)
- State(状態が書かれたメモ帳)
- widget ツリー(build の return に書かれた Widget 達)
まずはコードを見てから、それぞれの役割と生成タイミングを説明します。
コード例
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text("count = $count"),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: Text("増やす"),
),
],
);
}
}
4つの登場人物
① StatefulWidget(=設計図)
- UI の設計図にすぎない
- 設計図の内容は「State を作るという事実だけ」
- アプリ中ずっと変わらない
- 最初の1回だけ生成される
② Element(=UI の実体・司令塔)
- コードには登場しないが、Flutter が内部で必ず作る
- UI の実体であり、司令塔
- 親子関係の管理、差分計算、描画指示を担当
- StatefulWidget が画面に置かれた瞬間に生成される(最初の1回だけ)
- 生成順は StatefulWidget → Element → State
③ State(=状態が書かれたメモ帳)
- Element が createState() を呼んで 最初の1回だけ生成される
countなどの状態はすべてここに保存されるsetState()は 「メモ帳の内容が変わったよ」と Element に知らせるだけ- State インスタンス自体は作り直されない → 中身だけ変わる
④ widget ツリー(=build の return に書かれた Widget 達)
return Column(
children: [
Text("count = $count"),
ElevatedButton(...),
],
);
- Column / Text / ElevatedButton などの Widget 達の集合
- build() が呼ばれるたびに作り直される
- 軽いデータ構造なので、作り直しても問題ない
print 入りコードで内部の順番を見る
class MyWidget extends StatefulWidget {
MyWidget() {
print("MyWidget: 生成された");
}
@override
State<MyWidget> createState() {
print("MyWidget: createState 呼ばれた");
return _MyWidgetState();
}
}
class _MyWidgetState extends State<MyWidget> {
int count = 0;
_MyWidgetState() {
print("_MyWidgetState: 生成された");
}
@override
Widget build(BuildContext context) {
print("build: count = $count");
return Column(
children: [
Text("count = $count"),
ElevatedButton(
onPressed: () {
setState(() {
count++;
print("setState: count = $count に更新");
});
},
child: Text("増やす"),
),
],
);
}
}
起動時に内部で起きていること
説明
- MyWidget(設計図)が生成される
- Element(UI の実体)が生成される
- Element が createState() を呼び、State が生成される
- State.build() が呼ばれ、widget ツリーが作られる
- Element が widget ツリーを元に描画を行う
コンソール出力
MyWidget: 生成された
MyWidget: createState 呼ばれた
_MyWidgetState: 生成された
build: count = 0
ボタンを押したときに起きていること(setState の正体)
説明
- State の中身(count)が変わる
- setState() が Element に「変わったよ」と通知する
- Element はコードには書かれていないが、通知を受けた瞬間に State.build() を呼び、widget ツリーを作り直す
- Element が前回の widget ツリーと今回の widget ツリーを比較して差分を計算する
- RenderObject が「変わった部分だけ」描画する
コンソール出力
setState: count = 1 に更新
build: count = 1
Flutter が高速な理由(差分描画)
- widget ツリーは毎回作り直される(軽いので問題ない)
- 描画は重い処理だが、Element が差分だけ RenderObject に伝える
変化していない部分は再描画されず、変わった部分だけ描画されるため高速。