Flutter の StatefulWidget はどう動く?Riverpod や Hooks を使う前に理解したい State・Element・Widget ツリーと setState の仕組み

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("増やす"),
        ),
      ],
    );
  }
}

起動時に内部で起きていること

説明

  1. MyWidget(設計図)が生成される
  2. Element(UI の実体)が生成される
  3. Element が createState() を呼び、State が生成される
  4. State.build() が呼ばれ、widget ツリーが作られる
  5. Element が widget ツリーを元に描画を行う

コンソール出力

MyWidget: 生成された
MyWidget: createState 呼ばれた
_MyWidgetState: 生成された
build: count = 0

ボタンを押したときに起きていること(setState の正体)

説明

  1. State の中身(count)が変わる
  2. setState() が Element に「変わったよ」と通知する
  3. Element はコードには書かれていないが、通知を受けた瞬間に State.build() を呼び、widget ツリーを作り直す
  4. Element が前回の widget ツリーと今回の widget ツリーを比較して差分を計算する
  5. RenderObject が「変わった部分だけ」描画する

コンソール出力

setState: count = 1 に更新
build: count = 1

Flutter が高速な理由(差分描画)

  • widget ツリーは毎回作り直される(軽いので問題ない)
  • 描画は重い処理だが、Element が差分だけ RenderObject に伝える

変化していない部分は再描画されず、変わった部分だけ描画されるため高速。

Leave a Comment

CAPTCHA