Flutterの記事を整理し本にしました
- 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
- 今後はこちらを最新化するため、最新情報はこちらをご確認ください
- 10万文字を超える超大作になっています(笑)
はじめに
FlutterでElementというものを度々目にするようになって、その後調べていったら、芋づる式に3つのツリーを勉強することになりました。
結構納得したので、整理してみました。
まとめ
本記事は、下記のGoogleDeveloperDaysの情報をベースとしています。
3つのツリー
FlutterではWidgetのツリーで要素が構成されるという話をしてきましたが、Widgetツリーがそのまま画面に表示されているわけではありません。
実は、3つのツリーが相互に連携しあうことで、画面の構築、状態管理、性能などを最適化しています。
それぞれの詳細説明に入る前に、なぜ3つに分けているのかと、大まかな役割について解説してきます。
3つのツリーに分離する目的
3つのツリーに分ける目的は、端的にいうとパフォーマンスの最適化です。
FlutterはWidgetで管理されており、頻繁に再構成され、なおかつそれぞれのWidgetは状態を持っています。
また、画面に表示するためには、実際のサイズやレイアウトを意識した調節も必要になってきます。
仮に1つのツリーで管理されている場合は、管理はわかりやすいかもしれません。
しかし、全体がツリー構造のため、根本に近いWidgetが変わった場合は、その子孫のWidgetをことごとく作り直しをして、レンダリングする必要があります。
Flutterでは、Widgetは大きなネスト構造となるため、これは性能劣化を引き起こします。
そこで、3つのツリーに機能を分け、それぞれの役割と責任範囲を整理することで、これらの問題を解決しています。
3つのツリーの概要
Widgetツリー
Widgetツリーはimmutable(不変)なオブジェクトで構成されています。
Widgetツリーは生成と破棄のコストが非常に小さいように設計されています。つまり、頻繁に作り変えられるWidgetツリーの更新に瞬時に対応できるように設計されています。
Elementツリー
Elementツリーはmutable(可変)なオブジェクトで構成されています。
Widgetツリーはimmutableなので状態を持ちません。
そのため、ElementはWidgetツリーに変わって状態を管理します。また、RenderObjectのライフサイクルについても管理します。
いうなれば、Widgetツリーに状態をもたらし、WidgetツリーとRenderObjectツリーの仲介する役割です。
RenderObjectツリー
RenderObjectツリーはmutable(可変)なオブジェクトで構成されています。
RenderObjectは画面のレンダリングと描画を担当します。
Flutterが実際に画面に表示する際に参照するのは、Widgetツリーではなく、このRenderObjectツリーです。
WidgetツリーとElementツリーの関係
WidgetツリーとElementツリーの対応
WidgetツリーとElementツリーは1対1に対応した要素を持っています。
これだと完全に冗長にみえるのですが、後述する状態管理において、意味合いが変わってきます。
状態管理
繰り返しになりますが、Widgetはimmutableで高速に生成と破棄されるため、状態を持ちません。
しかし、StatefulWidgetでsetStateをすると値が書き換わります。
これはElementツリーがその状態を管理しているため、実現できています。
StateObjectというものを作成し、状態を作り管理します。
状態の更新
setStateで値を変えた場合に何が起こるでしょうか。
setStateをすると、StateObjectがdirtyとなり変更の必要があることを認識します。その後、StateObjectが最新化され、Elementに対応するWidgetがbuildされ、再作成されるという流れになります。
この時、Elementツリーは再作成のコストが高いので、可能なものは極力再利用します。
WidgetはcanUpdateなどのメソッドをつかい、新旧のWidgetのtypeとkeyを使って、別物に変わったかなどをチェックしています。
また、UpdateRenderObjectメソッドでは、既存と同じものはそのままに、変わったところだけを更新するという、極力再利用する方針の処理が行われます。
ツリー構築
構築
HelloWorldを例に説明していきます。
HelloWorldでは、Centerの中にTextが入っているので、途中を省略すると、Centerが親でTextが子供の構造が作られます。
その後、Elementツリーが作られ、対応するRenderObjectツリーが順次作られていきます。
更新
次に、更新処理を見ていきます。
半分は、WidgetツリーとElementツリーの場所で見たものと同じです。StateObjectは省略しています。
Widgetツリーは瞬時に新しいオブジェクトに作り変えられ、それを受けて、参照先の変更と変更された分の要素の変更が伝播していきます。
ElementツリーとRenderObjectツリーは必要な限り再利用するので、Widgetツリーと異なり再作成されていないことがポイントです。
アプリの一番最初で行うrunApp
メソッドは、Widgetツリーの根本にそのWidgetを配置するという操作をしています。
確認
- HelloWorldでDevToolを使って確認してみます。
ボタンをクリックし、カウンタが増えてもRenderObjectIDが変わっていないことがわかるかと思います。
より現実的な例
より現実的に性能の問題などを考える場合は、同じツリー構造を持った者同士を切り替えられるかという点になってきます。
上述の通り、ElementツリーとRenderObjectツリーは可能な限り再作成を行わず、修正コストを小さく抑えようとしますので、下図のような、大きく内容が変わる場合であっても、同じWidgetツリー構造を持ってさえいれば、書き換えは最小限で済むように設計されているため、非常に高速であるといえます。