💬

Flutter build()メソッドに関して

に公開

おさらい

https://zenn.dev/rionishino/articles/0f14027f2304e8

WidgetやFlutterの「宣言的UI」について簡潔にまとめました。
次にwidgetを作成するbuild()メソッドについて簡潔にまとめていければと思います。
※これは僕が後輩にFlutterについてを伝える内容である。

build()メソッドに関して

結論として、build()メソッドは、「描画そのもの」ではなく、UIの設計図を作成する役割を担っています。

build()メソッドは、現在のアプリの状態に基づいて「UIがどのような構成であるべきか」を記述する。いわば、build()メソッドはWidgetの構成を返している。

class MyMessageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 1. buildメソッドの役割は「設計図(ウィジェットツリー)」を返すこと
    return Center(
      child: Container(
        color: Colors.blue, /// 青色背景
        child: Text('こんにちは!'),
      ),
    );
  }
}

build()メソッドが「何をしている」のか?

1.UIの記述

「青い背景のContainerの中にテキストが表示」されているというUIの状態を宣言。

2.Widget treeの構築

Centerの下にContainer、その下にTextが存在しているという「親子関係(widget tree)」を作成してフレームワークに返却しています。

画像だとこんな感じ
https://medium.com/@ffarzad45/understanding-the-flutter-widget-tree-20d3ed648dbe

build() = 描画ではない?!

build()メソッドが実行された直後、画面に色は塗られていません。
ソースに基づくと、実際に画面の表示までは以下のプロセスが裏側で動いています。

1. build(構築)

build()メソッドが走り、設計図(widget tree)が作成される

2 Layout

作成された設計図(widget tree)に基づき、サイズと位置が計算されます。
flutterのレイアウトを理解する上で重要な原則は以下三つとなります。

  1. 制約は下に流れる (Constraints flow down): 親ウィジェットが子に対して、「お前はこのサイズ内で収まるように」という制約を伝えます。

  2. サイズは上に上がる (Sizes flow up): 子ウィジェットは受け取った制約の中で自分のサイズを決め、親に「私はこの大きさになります」と報告します。

  3. 親が位置を決める (Parents set positions): 親は子のサイズを知った上で、最終的な配置場所を決定します。

生成AI作成

3.Paint (描画)

レイアウトが確定した後に、Flutterのレンダリングエンジン(Impeller等)が画面上のピクセルを塗りつぶします。ここが以前の設計図との違いを見て画面描画する部分となります。

StatefulWidgetでの挙動 (ここが重要!!)

では実際にどうなのかを見ていきましょう。
例えば、Flutterのカウンターアプリだと下記のようになります。

class MyCounter extends StatefulWidget {
  @override
  _MyCounterState createState() => _MyCounterState();
}

class _MyCounterState extends State<MyCounter> {
  int _count = 0; // これが「状態(State)」

  /// function
  void _increment() {
    setState(() {
      // 1. データを更新する
      _count++; 
    });
  }
  @override
  Widget build(BuildContext context) { /// ←ここにbuild()メソッドがある!!!
    // 2. setStateが呼ばれると、このbuildメソッドが再実行される
    return Column(
      children: [
        Text('ボタンを押した回数: $_count'),
        ElevatedButton(
          onPressed: _increment, // ボタンを押すとsetStateが走る
          child: Text('加算'),
        ),
      ],
    );
  }
}

setStateを呼ぶとFlutterは再度build()を呼び出します。

setStateが呼ばれた時に状態の書き換え

1.状態の書き換え
アプリ内部のデータを変更する。

2.フラグを立てる
flutterの内部で再構築(rebuild)が必要なものとしてフラグを立てます。

3.buildの再実行 (rebuildする)
次の描画のタイミングでFlutterは自動的にbuild()メソッドを呼び出します。

4.新しい設計図の作成
新しいデータを反映した新しい設計図(widget tree)が作成されます

5.差分を更新
「古い設計図」と「新しい設計図」を比較して変更があった部分だけを画面に反映させます。
(再描画する)

どこまでがbuild()メソッドの書き換え範囲?

再度buildして値が変わるとして、どこまでが影響範囲なのか?
setStateは画面の設計図(widget tree)を全て再構築するのか?

結論は、ツリー全体を再構築されるわけではありません。
先ほど差分だけ更新と触れた様にflutterはそれぞれ新旧見極めて再構築してくれます。

カウンターの例で説明します。(良い例)

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('全体には影響しない')),
      body: Column(
        children: [
          Text('ここは親なので再構築されない'),
          MyCounter(), // ← ここの中で setState が呼ばれる
        ],
      ),
    );
  }
}

MyCounter内でsetState()を呼んでも、MyCounter内で完結しており、親であるMyPageのbuild()メソッドが走り直すことはありません。

では仮に親でrebuildした場合はどうなるでしょう? (悪い例)

class BadExample extends StatefulWidget {
  @override
  _BadExampleState createState() => _BadExampleState();
}
class _BadExampleState extends State<BadExample> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    // ボタンを押すと、重い部品(HeavyWidget)も含めて全部作り直しになる
    return Scaffold(
      body: Column(
        children: [
          HeavyWidget(), // 本来は再構築不要
          Text('$_counter'),
          ElevatedButton(
            onPressed: () => setState(() => _counter++),
            child: Text('増やす'),
          ),
        ],
      ),
    );
  }
}

BadExample内でsetStateをすることにより、build()メソッドが走ります。
その場合、先ほどと違いHeavyWidget()もrebuild対象になります。
HeavyWidget()内部に変更する値が無くてもです。

なぜかというと、setStateはbuild()メソッドを呼び出します。
build()された場合は、配下のものは全て再構築されるのです。
Scaffold配下のColumn、HeavyWidget、Text、ElevatedButtonが再構築対象。

再構築後、新旧の設計図の差分を確かめて差分がある部分の再描画をflutterは行います。
そして、Textの差分を見つけて、その部分だけ再描画するのをflutterが行なっています。

非効率では?と思うかもですが、Flutterは不変のwidgetであるため再構築の方が圧倒的に高速で効率的です。Flutterだからできる「技」なのです。

ただ、rebuildの範囲を少なくして行えるかが公式でも記載あるベストプラクティスとのことでした。

まとめ

1.build()メソッドはWidgetの構成を返している。
2.setStateの宣言場所に気を付ける!
3.再構築からの再描画で値が変わる!

以上!!!

次はElementについて

https://zenn.dev/rionishino/articles/32a38582008cba

Discussion