🎄

日本人のための Inside Flutter 用語解説

2021/12/25に公開

この記事は Flutter アドベントカレンダー 2021 最終日の記事です。

Flutterはカレンダー3までほぼ全ての日程が埋まってしまうほどの盛り上がりでしたね!

ラストはさらに Flutter への理解を深めていくためのとっかかりになる記事をと思って書いてみましたので、読んでみてください。


Flutter は公式ドキュメントがとても充実した技術です。

flutter.dev にはチュートリアルやAPIリファレンスだけでなく、Flutter のアーキテクチャの解説や描画の仕組みなど、 Flutter という技術がどう成り立っているのか という部分についてもとても丁寧に説明されています。

Inside Flutter はその中でも アプリ開発者が配置した Widget が画面に描画されるまでの仕組み [1] を中心に解説しており、安全で高速に動作するアプリを開発するためにとても役立つ知識が書かれています。

https://docs.flutter.dev/resources/inside-flutter

しかし、一度読んでみるとわかるのですが、解説に使われるキーワードは Aggressive composabilityLinear reconciliation など、日本人には少し馴染みのない、うまく日本語訳して理解するのが難しい単語が多く使われています。(と私の英語力では感じました)

そこでこの記事では、それぞれのキーワードがどのような意味合いで使われているのかを、ドキュメントの内容を踏まえながら解説していきたいと思います。

この記事で Inside Flutter の読みづらい部分を解消し、年末年始のお休みにじっくり全文を読んで Flutter に対する理解を深める手助けになれればと思います。

Inside Flutterの構成

Inside Flutterには主に以下の3つの話題が書かれています。

  1. UIのビルド処理の最適化の仕組み - Aggressive composability
  2. 無限スクロールを実現するためのビルドの仕組み - Infinite scrolling
  3. APIデザイン - API Ergonomics

この記事では、その中でも最初の "Aggressive composability" のセクションに着目し、その小見出しとで使われる以下のキーワードについて解説します。

  • Aggressive composability
  • Sublinear layout
  • Linear reconciliation
  • Tree surgery

これらのキーワードを理解するためには、個々の単語をただ英和辞典で調べるだけでは不十分です。その段落で説明されていることを確認することで、それらが何を指すのか、どのような概念なのかを理解することがこの記事の目的です。

諸注意

この記事ではこれらのキーワードを「正確に訳す」ことを目的としているわけではありません。同じ単語に別の訳をあてている記事もあるだろうと思います。大事なのは、その言葉が指し示す意味や考え方であって、訳そのものが統一されている必要はないと思っています。という姿勢は先に明確にしておきたいと思います。[2]

また、この記事は Inside Flutter の日本語訳版ではありません。あくまでドキュメントの「英語的なとっつきづらさ」を解消するための記事です。最終的には Inside Flutter の原文を読み込むことをオススメします。

では、ひとつずつ考えていきましょう。

用語解説

Aggressive composability

まずは今回解説する内容を表す "Aggressive Composability" です。

一応それぞれの単語を日本語訳すると、 Aggressive(積極的な) Composability(構築可能性) という感じでしょうか。といってもこれだけ訳してもやはり何のことかよくわからないですね。

それでは、内容を読みながらその意味をつかんでいきましょう。

まず真っ先に書かれている通り

One of the most distinctive aspects of Flutter is its aggressive composability

とのことで、 "aggressive composability" というのは Flutter の最も大きな特徴のひとつだそうです。

その先には Widget に関する記述が続きます。

Widgets are built by composing other widgets, which are themselves built out of progressively more basic widgets. For example, Padding is a widget rather than a property of other widgets

Flutter において Widget とは、ボタンやテキストなどの画面に表示するパーツだけでなく、パディングや中心寄せなどの、他のフレームワークでは View のプロパティとして扱われることの多い要素も含みます。"Everything is a widget" という言葉の通りですね。

結果として、Flutter を利用して UI を組み立てるためには大量の Widget を使うことになります。

アプリ開発者が直接触るのは多くの場合この Widget ですので、APIデザインとしてこの Widget が気軽に使えなければ Flutter における開発体験(DX)が低下してしまいます。

そのため、使うべき Widget を慎重に検討しておっかなびっくり配置していくのではなく、 アプリ開発者は 作りたい UI を実現するための豊富な Widget を「自由にガンガン」(aggressive)「配置していける」(composability)仕組み を提供するのが Flutter の基本的な考え方である、というわけです。

それを達成するための工夫として、以下に挙げるような最適化が Flutter フレームワークに施されています。

では、それらを1つずつ見ていきましょう。

Sublinear layout

Sublinear については「劣線形」という訳語が数学では使われている らしい です。[3]

「線形」というのは「比例」とほぼ同じようなイメージで、ここでは例えば Widget の数が増えればそれに比例して処理量が多くなるようなことを言います。それに対して「劣線形」というのは、 Widget の数が増えた場合に同じだけ処理量が増えるとは限らない ことを言います。

先述の通り、Flutter はアプリ開発者が Widget を気軽に自由に配置して UI を構築できることを意識してAPIがデザインされている技術です。

そのため、Widget の数の増加がそのままアプリのパフォーマンスに影響してしまうような作りでは、結局はアプリ開発者が Widget の数を切り詰めることに意識を向けなければならなくなってしまいます。

そのような事態を防ぐための、 Widget の数の増加がそのままパフォーマンスの低下に影響しないための2つの工夫がここでは書かれています。

  • RenderObject によるレイアウト計算処理の最適化
  • Element によるビルド処理の最適化

それぞれについて、少し詳しく見ていきましょう。

RenderObject によるレイアウト計算処理の最適化

Of paramount importance is the performance of layout, which is the algorithm that determines the geometry (for example, the size and position) of the render objects.

当たり前のことですが、Flutter に限らず GUI を実現するフレームワークは「どこに何を描画すれば良いか」をピクセル単位で常に計算しています。アプリ全体のパフォーマンスを向上させるために、この計算処理の最適化はとても重要な課題です。

フレームワークによってはこの計算量が O(N²) で増えていくものもあるそうですが、Flutter はこれを O(N) に抑える、つまり "Linear" な計算量になっています。

またそれだけではなく、 前回のレイアウト計算結果をキャッシュし適切に再利用することで2回目以降の計算量を "Sublinear" にする 最適化の工夫がここでは説明されています。

Typically, the amount of time spent in layout should scale more slowly than the number of render objects.

そのため、Widget(正確には Widget によって生成される RenderObject)の数が2倍に増えたからといって、処理量も常に2倍に増加するわけではない、ということです。

RenderObject によるレイアウト計算の流れを簡単に説明すると、親となる RenderObject は子の UI の描画のために確保できる「枠」を表す Constraints を子の RenderObject に渡し、子はその内容に従ってレイアウト計算を行い、親に返却します。もし孫がいる場合は、親に計算結果を返却する前に自身に定義された Constraing を孫に渡して計算結果を求めます。最終的には RenderObject ツリーの末端から親へ親へとレイアウトの計算結果を返却することで、「行って」「帰る」の1往復でレイアウト計算を完了させます。これがレイアウト計算の処理量を "Linear" にする工夫です。

さらに、そこで得られた計算結果は各 RenderObject にキャッシュされ、「Constraints に変化がなければ計算結果は同一になるはずだから、その場合は再計算をせずキャッシュを返却する」というアイデアに基づき2回目以降のビルドでは計算結果を再利用することで、 "Sublinear" を実現します。

このあたりの詳細については Inside Flutter の Sublinear layout のセクションを読んでみるか、以下の動画で分かりやすく説明されています。

https://www.youtube.com/watch?v=jckqXR5CrPI

Flutter はスムーズなアニメーションを実現するために、最大 60fps という速度 でリビルドが繰り返されることが想定されています。その速度に耐えるために、 RenderObject には

  • 初回のレイアウト計算を "Linear" な計算量で
  • 2回目以降のレイアウト計算を "Sublinear" な計算量で

行うための最適化が実装されている、ということがここで使われている "Sublinear layout" という用語の意味です。

Element によるビルド処理の最適化

Similar to the layout algorithm, Flutter’s widget building algorithm is sublinear. After being built, the widgets are held by the element tree, which retains the logical structure of the user interface.

Flutter フレームワークの中心は RenderObject でも Widget でもなく、 Element です。

Widget(特に StatelessWidgetStatefulWidget)は他の Widget を build() によって生成しますが、その結果を管理するのが Widget によって生成される Element の役割です。

Widget はリビルドによって高速に生成と破棄が繰り返されることが想定されているオブジェクトです。そのため、リビルドによって毎回全ての Widget が再生成され、それに従って RenderObject が再生成され、レイアウトが再計算されるようでは、やはりパフォーマンスを向上するためになるべく Widget を使わない検討、またはなるべくリビルドを発生させない検討をアプリ開発者がしなければならなくなります。

そこで、 「どこからどこまでをリビルドすべきか」を最適に判断する 役割を Element が担います。

Flutter は状態の変化に応じてリビルドを発生させることで UI を変化させる仕組みとなっていますが、この「状態が変化した」結果フレームごとのリビルド対象になるかどうかのフラグを保持するのも Element の役割です。

また、リビルド対象になった場合、ツリーのどこまでをリビルドすべきかを判断するのもやはり Element です。

この Element がリビルドの範囲を適切に管理することによって、たとえ Widget が増えたとしても UI の更新処理の計算量が "Linear" ではなく "Sublinear" での増加にとどめる工夫をしていることがここでは説明されています。

このあたりは拙著「内側から理解するFlutter」本でも解説していますので、興味があれば無料部分から読んでみてください。

https://zenn.dev/chooyan/books/934f823764db62

以上、 RenderObjectElement による最適化により、 Widget の増加がそのまま計算量の増加に直結するのではなく、計算結果やオブジェクトの再利用により処理量の増加が最低限に抑えられる ことによって我々アプリ開発者が「積極的に」 Widget を配置していけるように工夫されています。

Linear reconciliation

"reconciliation" とは、「調整」「照合」という意味があるそうです。

ここでは、 Widget のリストがリビルド前後でどう変化したのか、どれが変化なし(再利用する)でどれが変化した(Element や State を再生成する)のかを「照合」する処理の最適化がここで説明されています。

たとえば

Column(
  children: [
    const Text('Hoge'),
    const SizedBox(height: 24),
    Image.network(user.imageUrl),
    const SizedBox(height: 40),
    TextButton(),
  ],
),

という構成の children を持つ Column がリビルドによって以下のように変わった場合

Column(
  children: [
    const Text('Hoge'),
    const SizedBox(height: 24),
    const Text('画像なし'),
    TextButton(),
    const _ReloadButton(),
  ],
),

このリビルド前後の children のどこが変化し、どこが変化していないのかを検出することは無駄なリビルド処理を削減する観点からも重要です。

これを実現するためのアルゴリズムとして、Flutter では一般的に採用される tree-diffing algorithm ではなく、 O(N) の計算量で処理できる別のアルゴリズムを採用していることが短い文章で説明されています。

Tree surgery

"surgery" は「外科手術」といった意味があるそうです。また、ここでの "Tree" は Element ツリーのことを指します。

Reusing elements is important for performance because elements own two critical pieces of data: the state for stateful widgets and the underlying render objects.

Flutter には、一度生成した Element とその子孫(サブツリー)を一度 Element ツリー本体から切り離し、別の場所に貼り付ける仕組みが備わっています。

これにより、 RenderObject によるレイアウトの再計算や Element とその子孫の再生成処理を省けるだけでなく、場合によっては State を保ったまま指定した UI パーツの配置場所を変化させることが可能です。

具体的には、 GlobalKey を Widget の key プロパティに渡すことで、その GlobalKey と Element がどこからでも参照できる場所にペアで保持されます。そして、必要に応じて GlobalKey を通して生成済みの Element にアクセスし、それを Element ツリーの任意の場所へ移動させることができるようになっています。

Developers can perform a non-local tree mutation by associating a GlobalKey with one of their widgets. Each global key is unique throughout the entire application and is registered with a thread-specific hash table.

先述の通り、 RenderObject は親から受け取る Constraints が同一である限り、一度計算したレイアウトを再計算することなく親へ返却します。つまり、この "Surgery" の仕組みによってある UI パーツの配置場所が変わったとしても、もし新しい親が昔の親と同じ Constraints を渡してくる場合は再計算をスキップすることができるわけです。

The render objects in the reparented subtree are able to preserve their layout information because the layout constraints are the only information that flows from parent to child in the render tree.

調査する時間がとれず推測ではありますが、この仕組みを利用した Widget の例が Hero なのではないかと思います。

Hero を使うことで画面遷移前後に表示される同一の Widget をいい感じにアニメーションで次の画面に持ち越すことができますが、それはこの "tree surgery" の仕組みを使って状態やレイアウトを保ったまま次の画面に持ち越すような仕組みになっているのではないかと考えています。いつかコードを読んで調べてみたいですね。

まとめ

以上です。

この記事では Inside Flutterで使われる耳慣れない英単語の意味をその段落の内容から解説しつつ、 Inside Flutter を読むための「とっかかり」を作りました。

ここまで説明した通り、Flutter にはアプリ開発者が快適に Widget を配置していけるよう、実装や設計、またAPIデザインの面でもいろいろな工夫が施されています。特に、Widgetやリビルドの増加が即パフォーマンスの低下につながるわけではないことがなんとなく伝わったのではないかと思います。

最初に書いた通り、この記事の内容は Inside Flutter の記述すべてを日本語訳したものではないため、さらに詳しく正確に Flutter への理解を深めるためにも一度 Inside Flutter の原文を最初から読んでみるのがおすすめです。

最後まで読むにはだいぶ時間がかかるかもしれませんが(私は1週間くらいかかった記憶があります)、読んで理解できれば Flutter アプリのコーディングにおける「これはやっても大丈夫」「これはこうしておいた方がよさそう」といった品質を向上させるための観点がいくつも身につくと思っています。

英語だから、機械翻訳しても要領を得ないからと敬遠してしまう状態が、この記事によって少しでも解消されれば嬉しいです。

脚注
  1. つまり Flutter のアーキテクチャにおける「フレームワーク層」の仕組み ↩︎

  2. というより、私の観測範囲ではここで使われている用語が他のドキュメントでも使われることはありません。そのため、Flutterの話をする上でこの単語自体を使うことはないだろうと思います。その意味でも、これらの用語の日本語訳として何という言葉を当てるか、という問題に神経質になる必要はないと考えています。 ↩︎

  3. らしい、というのは、私が典型的な文系出身でこのあたりの概念にとても疎くよく分かっていないためです。ある程度数学や情報工学をやられていた方ならこの項目は別に難しい内容ではないのかもしれません。 ↩︎

Discussion