🪶

Flutter 調査メモ〜描画の仕組み〜

2023/05/16に公開

目的

Flutter について勉強して2年経ちますが、まだまだ勉強することがあります。この記事では Flutter SDK について調査をし、学んだことを紹介致します。

なお、この記事では Flutter SDK についてのドキュメントと実際の Flutter SDK のコードとを合わせて調査しております。私独自の考えも入れておりますが、検証が十分でない点などあるかもしれません。正しい描画の仕組みに関しましては、どうぞ公式ドキュメントをご覧ください。あくまでも自分での調査の報告書としてどうぞご覧ください。

調査内容

Flutter は途中でいくつかのレイヤを挟んだり、外部が提供する OSS (Skia など)を利用して描画などを行っております。Flutter の描画方法は車の製造に近い部分があるかもしれません。車を製造する際にはまず、各部品を作成して、それらを大きいものから順に組み立てていって、最終的に車という製品ができます。同じように Flutter ではまずウィジェットを全て確認し、それを元にウィジェットの情報を書き出し、出来上がったものをウィジェットツリーというウィジェットの設計書を作成し、そして最後にそれを画面に表示して、ユーザが操作できるようにします。

しかし、これはネイティブアプリでのビルドに関してであり、 Flutter ではアプリを開発する際と、Web開発をする際の描画の方法は少し異なります。

アプリ開発の際

アプリ開発をする際に Flutter は大まかに表示ウィジェット、エンジン、OSインターフェースに分けることができます。
英語ですとそれぞれ Framework, Engine, Embedder になります。
それぞれの役割は以下のようなものになります。

Framework : 開発者が操作するウィジェットやアニメーションのレイヤ
Engine : レンダリングやテキストのレイアウト、Dart のランタイムなどの制御
Embedder : OS ごとに対応(ネイティブなプラグイン etc)

Flutter での開発を行う際はメインはこの Framework の操作になります。開発者はユーザ側でどのようなウィジェットが表示されるかを設計・実装し、最終的にこれのビルドを行うことで APK ファイルにまとめることができます。ここのフレームワークの部分は全てのプラットフォームで共通して Dart で書かれております。

Engine は、例えばテキストのレイアウトであったり、ラスタライズ(画像などを画素に直してユーザに表示)、Dart のランタイムなど、よりハードウェアに近い部分での処理を行います。この部分は C/C++ で書かれており、これも全てのプラットフォームで共通化されております。

そして、Flutter が際立つ特徴の一つである、マルチプラットフォーム対応であるのには、Embedder の部分が大きく関係してきます。Embedder では直接 OS の処理と結びついており、Engine で制作したレンダリングツリーやインプットなどを元に各 OS の API などに対応づけて呼び起こして、処理を実行させます。

Web開発の際

Web アプリはブラウザ上で起動するため、Embedder と Engine で担当していた部分をブラウザに任せることができます。ウェブにおける Flutter SDK の役割はウィジェットツリーを作るところまでで止まります。ビルドをする際には Dart で書いたコードを HTML, CSS Canvas, SVG に変換して実行します。

最終的にはユーザが全てビルドし、エクスポートする際に、コードは全て JS 形式にまとめ上げ、実際にデプロイできるようにしています。

描画の仕組み

以下のコードをご覧ください。


// 元々の Build 関数を上書きして Scaffold の中身を表示

Widget build(BuildContext context){
	// BuildContext : ビルドする場所に関するツリーの情報
	return Scaffold(
		// アプリの中身
	);
}

基本的にレンダリングは上のコードで言う build 関数で実行されます。まず、元々用意されていた build 関数を呼びます。この時、@override によってウィジェットツリーをここで上書きします。上書きすることで新たなレンダリングツリーを表示する準備ができました。
次に、引数として渡された context (レンダリングツリー)を元に、新しく作成するツリーを準備します。これによって画面にウィジェットなどが表示されます。

レンダリングツリーを用意する理由は、それによってウィジェットが構造化でき、ウィジェットがまとまるためです。

そしてこれらのレンダリングツリーは何らかのトリガーを元に(画面が移り変わった際 or setState())描画 or 再描画されます。

なぜそんなことするの?と思った方もいらっしゃるかもしれません。例えとして次のプログラムを見てもらいましょう。

num = 0;


Widget build(BuildContext context){
	return Scaffold(
		body: Center(
			child: TextButton(
				onPressed:() => num++;
				child:(
					Text("$num");
				),
			);
		);
	);
}

ダミーコードなので細かいことはどうぞご容赦ください。
ここで数字を押した際に表示される数字が上がっていくようなコードを作成しました。もしロジックを毎回反映させてしまうと、数字が変わっていない時も画面に反映させようとするため、無駄な処理が増えてしまい、全体の処理が遅くなってしまいます。

そこで Flutter はロジックと表示を分けて、ユーザや Flutter 側が指示しない限りレンダリングツリーの状態で表示し続けるというスタンスを取りました。これによって、変数が変わった時のみに表示に反映させることになりました。

考察

Flutter for web と React とを比較してみました。共通点としてはどちらもネイティブなアプリを作ることができます。また、Flutter for web がレイヤベースであるのに対して、React はコンポーネントの概念を採用しております。レイヤベースであることの利点には階層へのアクセスを制限できることや、変更点があった場合に即座に更新できることが挙げられます。一方でコンポーネントベースでも多少変更が難しいとしても、レイヤベースでのレイヤ同士のような複雑な関係を持たなくて済むため、コードがコンパクトに抑えられることなどが挙げられます。

また、コードに関してはフレームワークの部分で限った話をすると、比較的ファイルの量が少ないように思えました。もちろんそれでもかなりレンダリングの処理などに関しては大量のファイルを必要としておりますが、原則機能を示すフォルダの中に割り当てられており、そのフォルダ内もせいぜい 10ファイル程度しかなかったため、全体として構造化がしっかりされておりました。

このようにレイヤベースならではのメリットも感じることができました。

最後に

この記事では Flutter SDK の構造についてドキュメントと SDK 本体のコード双方を読み、構造について自分が理解したメモになります。検証はしたものの、不確かな情報や、誤った結論などあるかと思います。それで今後継続的に学習を深めて、記事の内容を更新しようと思います。

参考文献

https://qiita.com/kurun_pan/items/02b46e4b330b137da3db
https://docs.flutter.dev/resources/faq#what-programming-paradigm-does-flutters-framework-use
https://github.com/flutter/engine/

Discussion