👌

【Flutter】Widget の build がどのように呼ばれるかトレースしてWidgetツリーを理解する

2023/02/24に公開

この記事では Widget.build がどのように呼ばれているのかを調べていきます。

どのように呼ばれているかを理解することで、エラーが出たときにちゃんと対処できそうかなと、なんとなく思いまして...

ところで、Flutterに入門してWidgetについて調べていると、Elementというものが登場します。

Each element represents a specific instance of a widget in a given location of the tree hierarchy.
引用: architectural-overview#build-from-widget-to-element

翻訳)

各Elementは、ツリー階層の特定の場所にあるWidgetの特定のインスタンスを表します。

さっぱりわからないですが、Widgetと密接に関係してそうです。

そして、上記のツリー階層を説明するのによく出てくるのが、以下のイメージです。

Widget/Elementツリー

引用: architectural-overview#build-from-widget-to-element

なるほど...。WidgetツリーとElementツリーとRenderツリーという3つのツリーがあって、Widgetに対して1対1で紐付いているものがElementなんですね。

Elementは2種類のクラスがあるようです。

  • ComponentElement
    -- 他のElementを構成するためのElement
  • RenderObjectElement
    -- レイアウトや描画に関わるElement

buildを呼ぶのはStatefulWidgetStatelessWidgetなどですが、それを参照するElementはComponentElementのようです。(実際はComponentElementを継承しているStatefulElementStatelessElement)

Widget.build() が呼ばれるところをトレースしてみる

とりあえず runApp のメソッドを追って、上記のツリー階層が形成されることを確認しました。
そうすることで、どこで Widget.build() が呼ばれるのかがわかるかと思ったからです。
よくあるサンプルプロジェクトで追いました。

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

親子関係は、親 → 子とすると
MyApp → MaterialApp → MyHomePage
となっています。

以下が、ツリー形成に関わってそうなメソッドをトレースした結果です。

runApp
↓
WidgetsFlutterBinding.scheduleAttachRootWidget(MyApp)
↓
WidgetsFlutterBinding.attachRootWidget(MyApp) // MyApp = rootWidget
↓
RenderObjectToWidgetAdapter.createElement()
    ↓
    ルートのElementが生成される (RenderObjectToWidgetElement)
    ↓
    RenderObjectToWidgetElement.mount
    ↓
    RenderObjectToWidgetElement._rebuild
    ↓
    RenderObjectToWidgetElement.updateChild
    ↓
    RenderObjectToWidgetElement.inflateWidget
    ↓
    RenderObjectToWidgetElement.newWidget(=MyApp).createElement()
        ↓
        StatelessElement(MyApp).mount
        ↓
        StatelessElement(MyApp).rebuild
        ↓
        StatelessElement(MyApp).performRebuild
	    ↓
            MyApp.build → MaterialApp (ここで子Widgetが生成される)
            ↓
            StatelessElement(MyApp).updateChild
            ↓
            StatelessElement(MyApp).inflateWidget
            ↓
            StatelessElement(MyApp).newWidget(→MaterialApp).createElement
	        ↓
                StatefulElement(MaterialApp)が生成される
                ↓
                StatefulElement(MaterialApp).mount
                ↓
                ...

ざっくりまとめると、

ルートのElement生成

Widgetを参照するElementが生成される

Elementが参照するWidgetのbuild実行(子Widget生成)

子Widgetを参照する子Elementが生成される

...

みたいな感じでしょうか。ツリーになってそうです。

なので、 Widget.build() は Element が呼び出しています。
そして、build の引数である BuildContext は、Element自身でした。
たとえば StatelessElement の実装をみると以下のように自身を渡しています。

Widget build() => (widget as StatelessWidget).build(this);

・・・

本記事を書くにあたって、以下の記事を特に参考にしました。

https://medium.com/flutter-jp/dive-into-flutter-4add38741d07

こちらの抜粋になりますが、

ルートからWidgetという設計図を頼りに上からElementツリーがじわじわ形成されていくイメージです。

ツリーが形成される様子をとても的確に表現されていてすごいと思いました。

描画に関わるRenderObjectElementを含むElementが生成されると、描画に関わる処理が走るかと思われますが、そこはできたら別記事で追えたらと思います。(できたら)

ありがとうございました!

Discussion