📱

[Flutter入門]Widgetからピクセルまで「どう動いているか」をざっくり理解する

に公開

概要

Flutterを触り始めると、
「build が呼ばれてWidgetを返しているのは分かるけど、その後どうやって画面が描かれてるの?」
というモヤッとした疑問が出てきがちです。

この記事では、

Dartで書いたWidgetが、どういう流れで画面上のピクセルになるのか

をテーマに、Flutterの基礎的な仕組みを図とサンプルコード多めで整理していきます。

想定読者

  • これからFlutterでモバイルアプリを作りたいエンジニア
  • Android / iOS / Web などの開発経験はあるが、Flutterの内部構造はまだよく分からない人
  • 「Widgetを書いているけど、この裏で何が起きているのか知りたい」と感じている人

1. Flutterとは何者か ー 自前で描画するUIフレームワーク

Flutterは、DartでUIを宣言的に書けるマルチプラットフォームUIフレームワークです。

  • 1つのコードベースから、Android / iOS / Web / デスクトップ などに対応
  • OS標準のUIコンポーネントに頼らず、自前のレンダリングエンジン(Skia / Impeller)でピクセルを描画する

特に最近は、Impeller という新しいレンダリングエンジンがデフォルト化されつつあり、

  • シェーダコンパイルによるカクつきを減らす
  • GPUを効率よく使って、滑らかなアニメーションを目指す
    といったところが特徴になっています。

2. Fluuterのレイヤー構造をざっくり把握する

まずは「大まかにどんな層でできているのか」を押さえておく必要があります。
公式のアーキテクチャ概要では、Flutterはざっくり次のレイヤーで説明されています。

ざっくり表現すると、

という流れで動いています。

3. 「3つのツリー」で考える:Widget/Element/RenderObject

FlutterのUIを理解するうえで、3つのツリーを押さえておくと一気にスッキリします。

  • Widget Tree
  • Element Tree
  • RenderObject Tree(Render Tree)

3-1. Widget Tree ー UIの「設計図」

Widgetは 「こういう見た目・こういう構造になっていてほしい」 というイミュータブルな設計図です。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Intro',
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Hello Flutter')),
      body: const Center(
        child: Text('Hello World'),
      ),
    );
  }
}

上記コードでの Widget Tree は以下のイメージになります。

MaterialApp
  └─ MyHomePage
       └─ Scaffold
            ├─ AppBar
            │    └─ Text("Hello Flutter")
            └─ Center
                 └─ Text("Hello World")

Widget自体は軽量で、毎フレーム作り直されることすらあります。
重要なのは、「Widgetはあくまで設計図である」という点です。

3-2. Element Tree ー 「場所」を表す橋渡し役

ElementはWidgetインスタンスと、そのWidgetが配置されている場所とを結びつけるオブジェクトです。

ポイントとして、

  • 各Widgetに対応して1つのElementが存在する
  • Elementは、Widgetの差し替え(更新)やライフサイクル(mount / update / unmount)を管理する
  • Widgetは頻繁に作り直されるが、「その場所のElement」は再利用される

イメージとしては、以下の通りとなります。

Widget Tree           Element Tree
────────────          ────────────
Scaffold       →      Element(Scaffold)
 AppBar        →      Element(AppBar)
  Text         →      Element(Text)
 Center        →      Element(Center)
  Text         →      Element(Text)

3-3. RenderObject Tree ー レイアウトと描画の本体

RenderObjectは、 「このノードはどのサイズでどこに描かれるか」というレイアウトと、「何をどう描くか」 という描画を担当します。

  • 多くのUIは RenderBox をベースにした RenderObject で構成される
  • 親から子へ「制約(constraints)」を渡してサイズを決める
  • ペイントフェーズで描画命令を積み上げる

4. 1フレームのライフサイクル:イベントからピクセルまで

では、ユーザーがボタンを押したり、setState を呼んだとき、
具体的にどんな流れで「画面が更新されるフレーム」が進むのでしょうか。

Flutter公式の「Life of a Frame」やレンダリングパイプラインの解説をベースにすると、流れはざっくり次のようになります。

1. 状態更新 / イベント
2. Build(Widgetツリー更新)
3. Layout / Paint / Compositing(描画内容の決定)
4. Rasterize(GPUでピクセルに変換)

4-1. 状態更新 / イベント

  • タップ・ドラッグなどの入力イベント
  • アニメーションの進行
  • setState() や Provider / Riverpod などによる状態変化

これらをトリガーに、 「この辺のUIを更新しよう」 というフラグが立ちます。

4-2. Buildフェーズ(Widgetツリーの再構築)

トリガーが発生すると、該当する build メソッドが呼ばれて、新しいWidgetツリーが作られます。

ここで重要なのは、

  • 毎回ツリー全体を1から作り直すのではない
  • Elementが「古いWidget」と「新しいWidget」を比較
  • 再利用できる部分を再利用し、変更箇所のみ差し替える

という最適化が行われている点です。

4-3. Layout / Paint / Compositing

Widget/Elementの更新が済むと、RenderObjectツリーに対してレイアウトとペイントが実行されます。

  1. Layout(レイアウト)
    • 親 → 子へ「この制約内でサイズを決めてね」とconstraintsを渡す
    • 子はその制約内で自分のサイズを決定し、必要ならさらに子にconstraintsを渡す
  2. Paint(ペイント)
    • Canvas ライクなAPIで描画命令が積み上がる
      • 「ここに矩形を描く」
      • 「ここにテキストを描く」
      • 「ここに画像を描く」
  3. Compositing(コンポジティング)
    • 透明度、クリッピング、変形(Transform)などを考慮し、レイヤー構造(Layer Tree)にまとめ上げる

4-4. Rasterize(エンジンがGPUで描画)

最後に、Flutter Engine が Layer Tree を受け取り、
Skia / Impeller を通してGPUへ描画命令を送り、ピクセルとして画面に表示されます。

5. ここまでのまとめ

ここまでのポイントを整理します。

  • Flutterは、DartでUIを宣言的に書くマルチプラットフォームUIフレームワーク
  • アプリコード → Framework → Engine → OS というレイヤー構造を持つ
  • UIは以下の3ツリーで管理されている
    1. Widget Tree(設計図)
    2. Element Tree(場所とライフサイクル)
    3. RenderObject Tree(レイアウト & 描画)
  • 1フレームの流れとしては以下の通りである。
    1. 状態更新 / イベント
    2. Build(Widgetツリー更新)
    3. Layout / Paint / Compositing
    4. Rasterize(GPUで描画)
  • Element / RenderObjectを再利用することで、無駄なコストを抑えつつUI更新を行う

最後に

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

現在、Flutter × Railsによるモバイルアプリの個人開発を行っているのですが、そもそも「Flutterってどういった流れで動作してんねんやろ...?」とお恥ずかしい状態となってしまっていました😅

ということで、今回は基礎中の基礎レベルの知見アウトプットという形で技術記事を書かせていただきました!

次に、 StatelessWidget/StatefulWidget・setState に関する技術記事を書こうと思いますので、よければ次回も読んでいただけますと幸いです☺️

Discussion