【Flutter】Widget の build がどのように呼ばれるかトレースしてWidgetツリーを理解する
この記事では 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ツリーとRenderツリーという3つのツリーがあって、Widgetに対して1対1で紐付いているものがElementなんですね。
Elementは2種類のクラスがあるようです。
-
ComponentElement
-- 他のElementを構成するためのElement -
RenderObjectElement
-- レイアウトや描画に関わるElement
build
を呼ぶのはStatefulWidget
やStatelessWidget
などですが、それを参照するElementはComponentElement
のようです。(実際はComponentElement
を継承しているStatefulElement
やStatelessElement
)
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);
・・・
本記事を書くにあたって、以下の記事を特に参考にしました。
こちらの抜粋になりますが、
ルートからWidgetという設計図を頼りに上からElementツリーがじわじわ形成されていくイメージです。
ツリーが形成される様子をとても的確に表現されていてすごいと思いました。
描画に関わるRenderObjectElement
を含むElementが生成されると、描画に関わる処理が走るかと思われますが、そこはできたら別記事で追えたらと思います。(できたら)
ありがとうございました!
Discussion