💧

Flutter の描画の仕組みを理解する

2022/03/14に公開
4

私個人の Flutter 歴も早数ヶ月程になってきたのですが、ちょくちょく雰囲気で使っているなーと感じるものが出てきたり(BuildContext や WidgetsBinding など)、描画のタイミングでバグが出た時にフワッとネットで見た記事通りに書いて解決したりなどの場面が出てきました。
それでもとりあえず動くアプリケーション開発はできなくないのですが、ちゃんと自信を持って開発したい & もう一段 Flutter 力を上げたいので本腰入れて Flutter がどう描画しているのかを調べてみたので、その結果をまとめてみました。
同じような課題感を抱いている方の足がかりとなれば幸いです。

はじめに: この記事のスコープ

最初にこの記事で話す「描画の仕組み」とは具体的に何を指すのかを述べます。
前提として、Flutter は大きく次の3つの層から成り立っています。

Flutter architectural overviewから引用

Framework は普段我々が Flutter で開発時に触っている部分で Widget やそれを支える仕組みを含んでいます。Engine の部分は低レベルな API の集まりで実際にスクリーンに描画する部分などを担当しており、これは dart:ui というパッケージとして Framework 側から参照されています。Embedder はプラットフォーム(iOS, Android, Web, Desktop など)ごとのロジックを含んだものです。

今回の記事の対象としているのは Framework の部分(+そこが Engine とやり取りをする部分)で、より具体的には

  • 我々が普段扱う Widget がどのようにピクセルへと変換されるか
  • 状態が変わった時の再描画はどのように行われるか

この2点を理解することをゴールとします。

対象読者

  • Flutter で多少は開発したことはあって、Widget とは何かはなんとなく理解している方。
  • Flutter というフレームワークが具体的にどうやって動いているのかを知ることに意欲がある方。

忙しい人のためのまとめ

かなり長い内容になるので、始めにすごい要約した内容を箇条書きでお送りします。

  • Flutter には Framework、Engine、Embedder の3つの層がある
  • Framework と Engine は Binding によってやりとりをする
  • Framework の部分は Widget, Element, RenderObject の3つでうまいことやって描画している
    • Widget は Configuration
    • Element は Widget のインスタンスで parent と children、Widget と RenderObject への参照を持ち、Flutter アプリケーションを成り立たせる上での中心人物
    • RenderObject はどのように描画をするのかについて責務を持つ
  • 状態が変わった時には再計算対象の dirty というフラグが Element につき、Frame 毎(この間隔は Engine によって制御されている)にまとめて再描画される

Framework と Engine はどうやり取りをしているのか

それではここから本文です。

まずは根っこの実際にピクセルをスクリーンに描画してくれている Engine と我々が実際に触る Framework がどうやってやり取りをしているか、それを学んでみたいと思います。

まず Engine と Framework がどうやってやり取りをしているのか、大枠を捉えるために以下の図をご覧ください。

Flutter internalsから引用

ここではまずジェスチャー(タップなど画面と関わる操作)や HTTP のリクエストなどによって起動されることから一連の処理が始まります。

Frame の間隔は Engine によって制御されていて、Frame 毎に Engine から DrawFrame という関数を実行して!と命令されます。そこから Framework の層では後述する Widget や RenderObject の更新をし、新しい見た目を再計算して最終的にまた Engine に「こういう風に画面を表示して!」というリクエストを行います。

このように Framework と Engine は繰り返しやり取りをしながら Flutter の描画は行われています。

Binding について

そしてこのやり取りは Binding と呼ばれるインターフェイスを介して行われています。この Binding を通してでしか Framework と Engine 間ではデータのやり取りは行われません。

Binding には次のような種類があります。

  • SchedulerBinding
    • 先ほどの Engine と Framework のやり取りをしている Frame の間隔の管理など
  • GestureBinding
    • ジェスチャー(画面をタップしたりとか)などのイベントを伝える役割
  • RendererBinding
    • Engine と Render Tree を繋ぐ役割
  • WidgetsBinding
    • Engine と Widget を繋ぐ役割

他にも色々あるのですが、気になる方はこの src のディレクトリの中のそれぞれの配下ディレクトリ(animation とかないのもありますが)にある binding.dart から読むことができます。
https://github.com/flutter/flutter/tree/master/packages/flutter/lib/src

Binding はそんなに直接触ることはないと思いますが、たまに便利なケースがあるので、その時 "Engine と Framework を繋ぐもの" という知識があるとより理解しやすいんじゃないかなと思ってます。

例えば、私も最近アプリがバックグラウンドからフォアグラウンドに変わった時の検知のために WidgetBinding を使って次のようなコードを書き、当時はおまじないのように使っていました。
今だと 「アプリが開いたかどうかはローレイヤーじゃないと分からへんから Engine から教えてもらってるんやろな。このメソッドは Widget の中で呼びたいから WidgetsBindingObserver ってのがわざわざ用意されてて扱えるようになってるんやろ。」とより確信を持って考えられるようになりました。

class _SomeState extends State<SomeWidget> with WidgetsBindingObserver {
    
  void initState() {
    super.initState();

    WidgetsBinding.instance.addObserver(this);
  }

  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    if (state == AppLifecycleState.resumed) {
      // アプリ再度開いた時に何かする
    }
  }
}

余談: WidgetsFlutterBinding について

ちなみに WidgetsFlutterBinding というのもあり、もしかしたら WidgetsFlutterBinding.ensureInitialized() というメソッドを使ったことをある方もいらっしゃるかもしれません。
ただ、これだけは「Engine と Framework を繋ぐもの」という役割ではなく、ざっくり表現すると "全ての Binding を初期化するもの" という役割を持っています。

何を隠そう、皆さんが必ず実行しているであろう runApp メソッドの中身はこの WidgetsFlutterBinding を使って Binding を初期化してからなんやかんやしています。

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

WidgetsFlutterBinding 自体のコードは下記のコードのみで非常にシンプルです。

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding._instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

ensureInitialized という関数があり、mixin を利用して各 Binding の initInstances メソッド(要するに初期化)を実行しています。
(ちなみにこのコード初見で見た時「何やってるんだってばよ?」と思ったのですが Dart の mixin という機能をうまく使ったものでした。こちらの解説記事(WidgetsFlutterBindingから見るmixinの使い方)が勉強になりました。)

Framework 内で描画の責務を持つ部分: RenderObject

Engine と Framework がどうやってやり取りするかを見てきました。
次に Framework 内で描画に責務を持つ部分はどこで何をやっているのかを具体的に見ていきます。

結論から言いますと、描画単体で重要な要素は RenderObject です(ちなみに root の要素は RenderView と呼びます)

Flutter internalsから引用

この RenderObject が具体的に何をやっているか、より理解を深めるためにコードを読んでみようと思います。

RenderObject は abstract class として定義していてより具体的なものとして extend する形で使われます。

色々メソッドは生えているのですが、何をやっているかを理解するには layout メソッドと paint メソッドを見ると分かりやすいかなと思います。

/// constraints を引数にレイアウトを計算する
// これを描画するぞ!というフラグを立てる
void layout(Constraints constraints, { bool parentUsesSize = false }) {
    ... 中略
    performLayout();
    markNeedsPaint();
}

layout メソッドでは引数に Constraints を取ってこの RenderObject を描画する layout を計算しています。また、最後に markNeedsPaint という関数を実行して、名前の通り "この RenderObject を描画する" ためのフラグを true にします。

結果として次のような paint メソッドが実行されます。(コメントを抜粋して翻訳しました)

/// 引数に指定された context, offset にこの RenderObject を描画します。
/// サブクラスではこのメソッドを override して、どんな見た目を描くかを実装します
void paint(PaintingContext context, Offset offset) { }

上記は abstract な定義なので実際に具象ではどんなことをやっているのか、Radio Widget の paint() がサイズ小さくてちょうどいい感じだったので紹介します。
(厳密に言うと下記では CustomPaint という Widget を使って描画しているのですが、PaintingContext を直接使っているコードでいい感じの例が見つからなかったのでご容赦ください。)


void paint(Canvas canvas, Size size) {
	paintRadialReaction(canvas: canvas, origin: size.center(Offset.zero));

	final Offset center = (Offset.zero & size).center;

	// Outer circle
	final Paint paint = Paint()
		..color = Color.lerp(inactiveColor, activeColor, position.value)!
		..style = PaintingStyle.stroke
		..strokeWidth = 2.0;
	canvas.drawCircle(center, _kOuterRadius, paint);

	// Inner circle
	if (!position.isDismissed) {
		paint.style = PaintingStyle.fill;
		canvas.drawCircle(center, _kInnerRadius * position.value, paint);
	}
}

https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/radio.dart

結果として描画されるのがこんな感じのものですね。

上記のコードと照らし合わせると本当に愚直に drawCircle などのメソッドを通じて Canvas を操作して丸を描いたりしているのが理解できるのではないでしょうか。

こんな感じで RenderObject と呼ばれるものがどんなレイアウト・座標でどんな風にピクセルを塗り潰すかを指定する責務を持っています。

ですが、我々が Flutter で開発していく時にこの RenderObject を触ることはほとんどありません。我々が扱うのは Widget ですね。
なので、次に Widget と RenderObject、そしてそれらを繋いでる Element について見ていこうと思います。

ちなみにこの layout や paint についての詳細なアルゴリズムについてはこちらの動画で解説されているのでご興味あればご覧ください。

https://www.youtube.com/watch?v=UUfXWzp0-DU

余談: Canvas って何

突然「Canvas を操作して描画している」という表現が出てきて、もしかしたら「え?Canvas ってなんのこと?」と思われた方もいらっしゃるかもしれません。

Flutter では基本的に描画エンジンとして Skia というものを採用しているので、モバイルなどをターゲットにしている場合はこれが使われます。(Web がターゲットの時はブラウザの Canvas)

https://skia.org/

下記の Flutter 公式チャンネルのコードリーディング動画では Skia のコードまで踏み込んで解説しているのでご興味あればご覧ください。

https://www.youtube.com/watch?v=qXAUNLWdTcw&t=183s

Widget, Element, RenderObject

さて、描画をする責務を持っているのは RenderObject だと解説しました。
ですが我々が Flutter で開発する時に RenderObject を直接触ることはほぼなく、全ては Widget として扱います。Everything is a Widget! ですね。
ここでは Widget、そのインスタンスである Element、そして RenderObject、それぞれの責務と、それぞれがどう関係しあっているかを解説していきます。

Element がこの3つを繋ぐ中心となっているので、

  • 最初に Element とは何ぞやを解説
  • Widget とは具体的に何か
  • そもそもなぜこの3つに分けられているのか

という順番で解説していくと分かりやすいかな?と思ったので上記の順で触れていきます。

Element とは?

An instantiation of a [Widget] at a particular location in the tree.
ソースコードより

https://github.com/flutter/flutter/blob/12bec573449b7c8655f30e66a973c6f8fb917062/packages/flutter/lib/src/widgets/framework.dart#L3145

Element は Widget をインスタンス化したもの、と Flutter のコード内のコメントでは書かれています。

一応 Flutter では3つのツリー(Widget tree, Element tree, Render Tree)がある、と解説されることが多いと思いますが、実際に親と子に参照を持っているのはこの Element のみです。
また、この Element は Widget と RenderObject に対しても参照を持っているので実質的にこの Element が Flutter の中心を担っていると言っても過言ではありません。

Widget, Element, RenderObject の関係性

図で紹介すると次のような形です。

先ほど申し上げた通り Element が中心にいて、Element から Widget と RenderObject それぞれに対して 一つ 参照を持っています。

このように他の要素間の参照を持つのが Element の役割となっています。

ちなみに我々が普段のアプリケーション開発で触るのは Widget ですが、これがどうやってこの Element を生み出しているのかについて軽く触れておきますと、Widget を継承したクラスがインスタンス化される時に Widget.createElement が実行されて Element が作成されます。また、次にフレームワークが mount メソッドを実行しその中で attachRenderObject が実行され RenderObject と結び付けられます。

余談: BuildContext について

ここで Flutter で開発していると必ず目にする BuildContext についても触れておきます。
なぜこのタイミングで?と思われた方もいらっしゃるかもしれませんが、何を隠そう、この BuildContext とはズバリ Element だからです。

先ほど Element は Widget をインスタンス化したもので親子に参照を持つモノとして紹介したところでピンとこられた方もいるかもしれません、BuildContext の目的は該当の宣言した Widget が Widget Tree の中でどこにいるかが分かるようにすることです。(ちなみに BuildContext class はコメントで A handle to the location of a widget in the widget tree. として解説されています。)

試しに Element の class 定義を見てみましょう。implements BuildContext しているのが見て取れますね。

abstract class Element extends DiagnosticableTree implements BuildContext {

BuildContext を implements してるのはこの Element だけなので「BuildContext は実質 Element!」と呼んでも差し支えないはずです。

ちなみに「なんでわざわざ BuildContext って分けてるの? Element そのままじゃダメなの?」と思われた方もいらっしゃるかもしれません。というか私は最初そう思いました。
これは憶測ですが BuildContext というインターフェイスで実際に開発者が触る部分を型付けすることによって Element Tree をうっかり書き換えちゃうような危険な操作をさせないようにする、という意図の API 設計なんじゃないかなと思っています。普段のアプリケーション開発で必要になる最小限の API だけ露出させているんじゃないかと。

ちなみにですが、「どうしても Element として操作したいんや!」という場合は実態は Element なのでシンプルに次のようにキャストしてあげれば動きます。

(context as Element).visitChildren()

補足として公式の BuildContext を解説している動画もオススメです。
https://www.youtube.com/watch?v=rIaaH87z1-g

更に余談: Element の種類

Element にはいくつか種類があります。

あまり知る必要のない知識感はありますが、もし Flutter のコードリーディングをしていく場合は、こういう違いがあるということを念頭に置いておくと、私の場合は見つけたい処理を見つけやすくなったので紹介しておきました。

Widget とは?

Flutter のソースコードを読むと

Describes the configuration for an [Element].

というコメントがついています。Element のコンフィグを表しますと書いてますね。
コンフィグってなんやねんと思われる方もいるかもしれないので何かしらの Widget の実装を眺めてみようと思います。

まず前提として、Widget には大きく次の4種類があります。

  • ProxyWidget
    • Proxy の名の通りデータを伝播するためのものです。InheritedWidget や ParentDataWidget などが継承しています。
  • RenderObjectWidget
    • 名前からお察しの通り RenderObjectElement 及び RenderObject をどう生成するかのコンフィグを含んでおり、見た目の部分にガッツリ関わります。
    • ここから更に LeafRenderObjectWidgetSingleChildRenderObjectWidgetMultiChildRenderObjectWidgetに分けられます。(名前からなんとなくお察しかと思いますが child を持っているか、複数持っているかどうかが違いです)
  • Components
    • 我々が普段開発する時に使うStatelessWidgetStatefulWidgetが該当します。他のプリミティブな Widget を組み合わせて作られます。

今回は Element と RenderObject の関係も見られると嬉しいかなと思うので RenderObjectWidget なものを見ていきたいと思います。
(余談ですが Flutter の標準 Widget のコードを読んでみると RenderObjectWidget を継承してなくても paint メソッドを実装ているものが多々見つかると思いますが、これらは CustomPainter という Widget を使って描画しています。RenderObject を直接使うのと CustomPainter を使うことの比較記事なんかもいつか書いてみたいなと思います。)

RenderObjectWidget は 次のように createElement メソッドで RenderObjectElement を返すのと createRenderObject 及び updateRenderObject というどんな RenderObject を作るのか/どうやってアップデートするのかを指定するメソッドを実装します。

abstract class RenderObjectWidget extends Widget {
  const RenderObjectWidget({Key? key}) : super(key: key);

  RenderObjectElement createElement();

  RenderObject createRenderObject(BuildContext context);
  void updateRenderObject(
      BuildContext context, covariant RenderObject renderObject) {}
  void didUnmountRenderObject(covariant RenderObject renderObject) {}
}

この RenderObjectWidget を継承している Widget として Container の中の DecoratedBox というものを見てみます。

class DecoratedBox extends SingleChildRenderObjectWidget {

  RenderDecoratedBox createRenderObject(BuildContext context) {
    return RenderDecoratedBox(
      decoration: decoration,
      position: position,
      configuration: createLocalImageConfiguration(context),
    );
  }
  
  void updateRenderObject(BuildContext context, RenderDecoratedBox renderObject) {
    renderObject
      ..decoration = decoration
      ..configuration = createLocalImageConfiguration(context)
      ..position = position;
  }
}

こんな感じで createRenderObject という RenderDecoratedBox を返す関数を実装していますね。そしてこの RenderDecoratedBox は RenderBox を継承していて下記のような paint メソッドを実装しています。
例が微妙に悪かった気がしないでもないですが(いい感じのが見つからなかった許して)、Canvas に対してどのようにピクセルを塗り潰すかを指定しています。

void paint(PaintingContext context, Offset offset) {
    _painter ??= _decoration.createBoxPainter(markNeedsPaint);
    final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
    if (position == DecorationPosition.background) {
      _painter!.paint(context.canvas, offset, filledConfiguration);
    }
    super.paint(context, offset);
    if (position == DecorationPosition.foreground) {
      _painter!.paint(context.canvas, offset, filledConfiguration);
    }
}

こんな感じで Widget がコンフィグ、具体的に言うとどんな RenderObject を描画するのかを指定しながら Element を作っているわけですね。

そして RenderObjectElement 側で mount メソッドの中で 上記のような Widget の createRenderObject メソッドを呼び出しています。

  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
        // ここ!
    _renderObject = (widget as RenderObjectWidget).createRenderObject(this);     attachRenderObject(newSlot);
    _dirty = false;
  }

また、この他にも Widget は状態やメソッドを保持したりなど、具体的に描画する時に必要な様々な情報を保持しています。このような形でどんな RenderObject を作るのか、どんなデータを渡して Element を作るのかなどなどの "コンフィグ" を保持するのが Widget の役割です。

なぜ3つに分かれているの?

という訳で Widget, Element, RenderObject という3つの役割から Flutter の描画は成り立っているよ、というのをお伝えしましたが、そもそもなぜこの3つに分かれているのでしょうか?

これに関しては多少憶測も入るのですが「宣言的 UI を実現するため & 実現しつつもパフォーマンスは劣化させないため」なのかなと思っています。

例えばですが、極端な話、命令的に操作するのであれば Element に直接どんな新しい要素を増やしたり削除したりするか書くので十分なはずで Widget は不要ですし、その場合は特に逐一 Element のような重い要素を削除する必要性はないためパフォーマンスの懸念もないでしょう。

ただ宣言的に書きたい場合は Element 単体で行くと状態がアップデートされた時に対象の Element 以下のツリーを全て再計算 & 描画に関わる重いオブジェクトまで破棄することになってパフォーマンスが毀損されてしまいます。

これを防ぐために最小限の更新と再利用を実現するために Widget と Element が分かれているのかなと思います。これに関してはGoogle の中の人が描画の仕組みを解説している動画の中で答えていたので、そこを参考に書くのですが、ツリーが変わった時には次のように Element と RenderObject を再利用しつつ更新をしています。

  • まず前提として Widget は Immutable で軽量なオブジェクトで、逐一破棄しても痛くありません。
  • ツリーが変わる時には新しい Widget が作られて Element では canUpdate というメソッドで新しい Widget と今までついていた Widget が同じものかが判定されます(ちなみにここでは runTimeTypekey が比較されています。なので可能な限り再利用するためにキチンとツリーの中で要素の位置が変わる時に key を指定して再利用できるようにすることが大事な訳ですね☝️)(補足: コメントでご指摘いただきましたが、key を指定しなかった場合でも 「リビルド前後でどちらも null なので同一である」と判定されるので「Widget の型は変わらないけど Element は再生成させたい」以外の場合にはつけない方が適切です)
  • 同一と判定された場合は Element の Widget への参照が新しいものに変わります。

このように宣言的に書けつつも、状態の更新時に必要な最小限の情報を Widget につめつつも最大限の情報を再利用できるようにするために Widget と Element + RenderObject に分かれているのかなと思います。

Element と RenderObject が分かれている理由については Flutter 公式のInside Flutterという記事に書いてあるので引用しますと次のように書かれています。

パフォーマンス。レイアウトが変更された場合、レイアウトツリーの関連する部分のみを walk する必要があります。composition により、Element ツリーにはスキップしなければならないノードが多数追加されることがよくあります。

明快さ。関係性の分離を明確にすることで、Widget プロトコルと RenderObject プロトコルをそれぞれ特定のニーズに特化させ、API を単純化し、バグのリスクとテストの負担を軽減することができます。

型の安全性。 RenderObject ツリーは、子オブジェクトが適切なタイプであることを実行時に保証できるため、よりタイプセーフになる(たとえば、各座標系には独自の RenderObject のタイプがある)。Composition Widget は、レイアウト時に使用される座標系にとらわれないことができます(たとえば、アプリ モデルの一部を公開する同じ Widget を Box Layout と sliver Layout の両方で使用できます)。したがって、Element ツリーでは、RenderObject の型を確認するために Tree walk が必要になります。

再描画の仕組み

というわけで Framework レイヤーの描画の仕組みは大体見てきました。
最後に、残っている謎である状態がアップデートされた時に 再描画 はどのように行われるのかを見ていこうと思います。

まず状態は StatefulWidget でしか扱えないのでその定義を見てみようと思います。(見やすさのためにコメントとか消してめっちゃ簡略化してます)

abstract class StatefulWidget extends Widget {
  const StatefulWidget({Key? key}) : super(key: key);
  StatefulElement createElement() => StatefulElement(this);
  State createState();   

普通の Widget と違う部分はほとんどありません。違うのは StatefulElement を返すところ、createState という State を返す関数を必ず override させるようにするところです。

お次にその State を見てみようと思います。

abstract class State<T extends StatefulWidget> with Diagnosticable {
  T get widget => _widget!;
  T? _widget;
  _StateLifecycle _debugLifecycleState = _StateLifecycle.created;
  BuildContext get context {
    return _element!;
  }
  StatefulElement? _element;
  bool get mounted => _element != null;
  Widget build(BuildContext context);
  
  void initState() {
    assert(_debugLifecycleState == _StateLifecycle.created);
  }
  void didUpdateWidget(covariant T oldWidget) {}
  void setState(VoidCallback fn) {
      final Object? result = fn() as dynamic;
        _element!.markNeedsBuild();
    }
  void didChangeDependencies() {}
}

実はすんごいシンプルなのがよく読むと分かると思いますが、まず State は Widget や Element などへの参照を持っていたり、Widget を返す build メソッドを持っています。これはおそらく State を継承している class で他の Widget のような書き味ができるようにするためでしょう。

そして肝は setState 関数です。ご存知の通り StatefulWidget を extends して実装している時は setState 内で状態を更新しますが、この関数がやっていることは至極シンプルで引数に渡ってきた関数を実行し、 _element!.markNeedsBuild(); という「この Element はビルド対象だぞ!」というフラグ(Element の中の dirty というプロパティ)を true にする関数を実行しているだけです。

そして、フラグを true にすると再描画がすぐに実行されるのかというとそうではありません。
一旦ここでは次に描画する Element のキューに詰められるだけで、次の Frame になった時にまとめて再ビルドが実行されます。これもまたパフォーマンス目的だと思われます。

ここで冒頭に紹介した framework と engine のやり取りの図を再掲します。

この Frame の間隔は Engine によって制御されていて、SchedulerBiding の handleDrawFrame という Engine から実行されるメソッドから更に WidgetsBinding を通して再計算のメソッド(drawFrame)が実行されます。

  • BuildOwner の buildScope というメソッドが呼ばれ、dirty にマークされた Element 全てに対して rebuild() というメソッドが呼ばれます。
  • その中で Element が紐づいている Widget の build メソッド(我々がいつも書いてるやつです!)を実行して(※厳密には ComponentElement の場合)新しい Widget が作られます。

こんな感じで状態が変わった時は ビルド対象としてマークされる -> Frame毎にまとめて再描画 という思いの外シンプルな仕組みで実現されていました。

おわりに

以上、Flutter の描画の仕組みを見てきました。
描画の仕組みを理解することによって Flutter Framework を使う上で扱う様々な要素の責務や意味が理解できるのでより自信を持って開発できるようになるのではないかと思います。
実際私も雰囲気で扱ってた WidgetsBinding や BuildContext などの意味がしっかりと分かったり、再描画の仕組みも分かったのでしょうもないエラーで沼ることもなく開発できています。

また、今回の記事では概要をお伝えしましたが、ぜひ実際に Flutter のコードリーディングをしてみることもお勧めします。私も他の方が似たようなトピックに対して書かれた記事や動画をたくさん見たのですが、実際にコードを見てどんな内容の関数が実行されているかをしっかり理解することによってより強い自信を持つことができるようになりました。

この本文の後ろに参照した記事/動画のリンクやコードリーディングで読むと良さそうなところをまとめてみたのでぜひお役立てください。

それでは、長文でしたがお読みいただきありがとうございました!!

リファレンス

この記事を書く上で勉強になった記事や動画のリンクを貼っていきます。

https://www.youtube.com/watch?v=UUfXWzp0-DU
https://www.alibabacloud.com/blog/exploration-of-the-flutter-rendering-mechanism-from-architecture-to-source-code_597285
https://www.didierboelens.com/2019/09/flutter-internals/
https://zenn.dev/chooyan/books/934f823764db62/viewer/3d3f8e
https://surf.dev/flutter-under-the-hood/
https://medium.com/flutter-community/flutter-what-are-widgets-renderobjects-and-elements-630a57d05208

Discussion

中條 剛(ちゅーやん)中條 剛(ちゅーやん)

とても勉強になる記事ありがとうございます!リファレンスもありがとうございます!

ひとつだけですが、

なので可能な限り再利用するためにキチンと key を指定することが大事な訳ですね☝️

この部分について、 key は指定しなければ 「リビルド前後でどちらも null なので同一である」と判断される作りになっているため、「Widget の型は変わらないけど Element は再生成させたい」という意図がある場合以外はつけない方が適切な仕組みになっています。

「キチンと key を指定する」というのが「すべてのWidgetに key をつけるべき」という印象を受けたため、コメントさせていただきました。

seyaseya

ありがとうございます!ちゅーやんさんの記事の数々大変勉強になりました。

この部分について、 key は指定しなければ 「リビルド前後でどちらも null なので同一である」と判断される作りになっているため、「Widget の型は変わらないけど Element は再生成させたい」という意図がある場合以外はつけない方が適切な仕組みになっています。

確かに null でも同一と判定されますね、この観点抜けておりました。
ご指摘ありがとうございます!記事の方でも補足コメントを追加いたします。

中條 剛(ちゅーやん)中條 剛(ちゅーやん)

すみません、、!私の書き方がまぎらわしかったですが、修正していただいた文章の

「Widget の型は変わらないけど Element は再生成させたい」場合にはつけない方が適切です

こちらは↑にコメントした元の文章では

「Widget の型は変わらないけど Element は再生成させたい」場合 「以外」 にはつけない方が適切です

です。なので、通常はつけない、ですね。
逆の意味になってしまうので、再度修正お願いできれば!

seyaseya

私もちゃんと考えないで鵜呑みにしちゃってました…ありがとうございます!後ほど修正します!