【Flutter】runAppからStatelessWidget.buildが実行されるまでの流れ

こんにちは、ぽたぽたです。
最近、Flutterでモバイルアプリの開発を始めました。

今回は、Flutterの理解度アップのために、
runAppが実行されてからStatelessWidgetのbuildが呼ばれるまでの流れを整理してみました。

少しでも皆さんの参考になれば幸いです!

ご意見や指摘等ありましたら、気軽にご連絡ください!(@potapota_kyoiku)

前提

私が前提として知っていたことが、
FlutterはまずWidgetからElementのツリー、RenderObjectのツリーを作って描画しているらしい、ということです。

それを前提知識として、その処理が実際にどうなっているのかを追ってみました。

おさらい

まずおさらいです。
FlutterのアプリはまずrunAppという関数が実行されるところから始まります。
その際、引数として自分で実装したWidgetを渡します。

その後、StatelessWidget.buildが実行されます。
次のサンプルコードを例に考えてみましょう。
 

まず、runAppが実行されます。
runAppの引数として、MyApp()が渡されています。

MyAppはその直後にclassとして宣言されていて、
StatelessWidgetを継承しています。

MyAppの中でbuildという関数が実装されており、
build内でreturnされているMaterialAppWidgetが実際に画面に表示されることになります。

今回はこのソースコードを題材に、
runAppが実行されてからStatelessWidget.buildに到達するまでの流れを調べました。

runAppからStatelessWidget.buildまでの流れ

まず、runAppからStatelessWidget.buildまでの流れを
単純に関数の呼び出し階層として列挙すると次のようになります。

runApp
WidgetsBinding.attachRootWidget
RenderObjectToWidgetAdapter.attachToRenderTree
RenderObjectToWidgetElement.mount
RenderObjectToWidgetElement._rebuild
Element.updateChild
Element.inflateWidget
Widget.createElement
  StatelessWidget.createElement
Element.mount
  ComponentElement.mount
ComponentElement._firstBuild
Element.rebuild
Element.performRebuild
  ComponentElement.performRebuild
ComponentElement.build
  StatelessElement.build
StatelessWidget.build

※インデントが下がっているものは、継承先の関数が実行されていることを表してます。

runAppから始まり、最終的にStatelessWidgetのbuildが呼ばれています。

これだけではまだ何もわかりませんが、
各関数が具体的にやっている内容ごとに関数を整理すると次のようなストーリーになります。

runAppが実行される

まず、runAppが実行されます。

runApp

Widgetをビルドするための準備をして、ビルドを始める

その次に、ルートWidget(runAppの引数で渡されたWidget)をビルドする準備が始まります。
ルートのWidgetはWidgetsBindingという場所にセットされ、
WidgetsBinding上でルートWidgetのビルドがスタートします。

Widgetのビルドは親Widget(親Element)の中でされます。
ルートのWidgetは親Widgetが存在しないので、WidgetsBindingがルートWidgetの親代わりになって、そこからビルドを始めているようです。

 WidgetsBinding.attachRootWidget
  RenderObjectToWidgetAdapter.attachToRenderTree
  RenderObjectToWidgetElement.mount

ビルドスタート

mountという関数の中で実際のビルドの処理が記載されています。
ここでいう「ビルド」とは、WidgetからElementを作ることを意味します。

_rebuildの中でupdateChildというWidgetからElementを作る関数が実行されています。(詳細は次)

 RenderObjectToWidgetElement._rebuild

WidgetからElementの作成

ElementはupdateChildという関数を持っていて、この関数にWidgetを渡すとそのWidgetからElementを作成して返してくれます。

updateChildの中でinflateWidgetという関数が呼ばれ、
inflateWidgetの処理のなかで、Widget.createElementという関数が呼ばれています。

 Element.updateChild
  Element.inflateWidget
  Widget.createElement
   StatelessWidget.createElement

WidgetごとにElementのつくり方は様々で、Widgetクラスに宣言されているcreateElement関数をそれぞれのWidgetクラスがoverrideして、それぞれのWidgetに応じたElement作成処理を実装しています。

Element作成の過程でmount処理

createElementの中でmountとうい処理が実行されます。

StatelessElementの場合、StatelessElementが継承している、ComponentElementのmountが実行されます。

ComponentElement.mountが主にやっていることは、
 ・自身が持つ親Element変数に親Elementをセット
 ・自分の子供のElementを作って、自身が持つ子Element変数にセット・・☆

子供のElementを作る際に、同様に子供のElementのmountが呼ばれ、
孫Elementが作られます。
このように再帰的にmountが実行され、Elementのツリーが構築されていきます。

Element.mount
 ComponentElement.mount
ComponentElement._firstBuild
Element.rebuild
Element.performRebuild
 ComponentElement.performRebuild
ComponentElement.build
 StatelessElement.build

子Elementを作るために子供のWidgetを取得

 StatelessWidget.build

子供のElementを作るために、まず子供のWidgetを取得する必要があります。

StatelessWidgetの場合、子供のWidgetはbuildによって取得できます。
このタイミングでStatelessWidgetのbuildが実行されます。

このあとの流れは省略していますが、
ここで作成したWidgetが上述のupdateChild関数によって、Elementに変換され、子Element変数にセットされます(上述の☆のポイント)。

runAppが実行されてから、StatelessWidget.buildに到達するまではざっくりこのような流れになっていました。

学んだこと

今回の調査でいろいろなことが理解できました。

1.Elementツリーの実装が確認できた

いろんなドキュメントにElementツリーの概念が書かれているが、
具体的なソースコードレベルで見てみると、

 class Element {
   Element _parent;
   Element _child;
 }

のようなイメージで親子を管理している。
Elementを作る過程で、子供のElementを作る。(updateChild)
同様に子供Elementを作る過程で、孫Elementを作る。
このような流れでElementツリーの構築は進む。

2.子供を持つElement、持たないElement

当然といえば当然なのかもしれないが、
すべてのelementが子供を持っているわけではない。
例えば、最終的に画面の描画に関わってくるようなRenderObject系WidgetのElementは子供を持たない。

StatelessWidgetやStatefulWidget、InheritedWidgetのElementは子供Elementを持つ。
ソースコードレベルの話になるが、
これらのElementはすべてComponentElementを継承しており、
ComponentElementが Element _child という変数を持っている。

3.updateChildで子Elementをつけはずししている

Element.updateChildで子供Elementをセットすると説明したが、
状態が変わった際の再描画の際もこのupdateChildが呼ばれている模様で(※要確認)、その際の子供の削除や変更部分の反映等もやっている。

最初の初期化のときだけを考えるのであれば、子供が無い場合は何もしなければいいだけだが、再描画の場合は話が変わる。
再描画前に子供があった場合はそれを削除する処理をしないと画面上子供が残り続けてしまう。

実際のソースコードでは、再描画前の子供のElementからwidgetを取得して、
今回描画するWidgetと比較して、新規、更新、削除を判定して処理している。

なるほどなー、と思った。

わからなかったこと・今後調べること

1.Widgetツリーの管理について

いわゆるWidgetツリーがどのように管理されているかまだわかっていない。

StatelessWidget自体は親も子も持っていない。
performRebuildの中でStatelessWidget.build()で作った
子Widgetはローカル変数に一旦入れているだけで保持はしていない。

Elementは明確にツリー構造をしていて、Elementが対応するWidgetを保持している。
だから、間接的にWidgetツリーを特定することは可能だが、これのことでいいのか??

2.RenderObjectツリーの管理について

Elementのツリーがどう構築されるかはわかった。次はRenderObjectのツリーがどうなっているか調べる。

3.実際の描画の流れ

今回の調査で画面の描画のための準備がどうなっているかわかった。
描画のために必要なElementツリー等の構築はなんとなくイメージができてきたので、そのツリーがどのように利用されて、画面が描画されていくのか。
そのあたりを理解する。

4.setStateの仕組み

画面の再描画を手軽にできる関数、setState。
Flutter開発者なら誰でも知ってる超便利関数。
その中身を理解する。

5.よく使うWidgetの継承関係を理解する

よく使うWidgetの継承関係をざっくり調べてみる。
今回、TextがStatelessWidgetを継承していることに気づいた。
別に驚くこともないんだけど、新たな発見だった。
よく使うWidgetの継承関係や中身の仕組みをざっくりでも把握しておけば、
更にFlutterでの開発の快適さが増しそうな気がする。

まとめ

今回はrunAppからStatelessWidget.buildまでの流れを整理しました。

フレームワークの中身は知らなくても直接困ることは少ないと思いますが、
コーディングに自信が出てきますし、いざというとき違いが出てきます。

今回の記事が少しでも皆さんのお役に立てたら幸いです。

もしこの記事が面白かった!と思っていただけた方は、下のシェアボタンからシェアしてもらえると嬉しいです!

twitterでもFlutter関連の情報を発信しているので、フォローお待ちしています!(@potapota_kyoiku

ここまで読んでくださり、ありがとうございました!