ビルドパイプライン(Widget 差分更新)
このページは Widget ツリーの差分ビルドの仕組み — BuildOwner / dirty list / BuildScope / Element.UpdateChild — を解説します。RenderObject 側の差分レイアウト・差分ペイントは RenderObjects を参照してください。
実装状況:
BuildOwnerとStatelessElement/StatefulElement/InheritedElement/SingleChildRenderObjectElement/MultiChildRenderObjectElement(Key対応の子リスト差分)/RenderObjectToWidgetElementの再ビルドが動作します。Key(ValueKey<T>/UniqueKey)はWidget.CanUpdateに組み込み済みです。
毎フレーム FloatSodaApp.MainLoop() から各ウィンドウの WidgetBinding.DrawFrame() が呼ばれ、次の 3 段階が実行されます。
sequenceDiagram
participant App as FloatSodaApp
participant WB as WidgetBinding
participant BO as BuildOwner
participant El as Element ツリー
participant PL as RenderPipeline
participant RT as レンダースレッド
App->>WB: DrawFrame()
WB->>BO: BuildScope()
Note over BO: dirty な Element を Depth 順にソート
loop dirty Elements
BO->>El: element.Rebuild()
El->>El: PerformRebuild()<br/>→ Build() → UpdateChild()
Note over El: RenderObject のプロパティ更新<br/>→ MarkNeedsLayout / MarkNeedsPaint
end
alt NeedsVisualUpdate == true
WB->>PL: FlushLayout()
WB->>PL: FlushPaint()
WB->>RT: PostRender(layer.Clone())
end
- ビルドフェーズ —
BuildOwner.BuildScope()が dirty な Element を再ビルドし、Widget ツリーの変更を RenderObject ツリーに反映します。 - レイアウト/ペイントフェーズ — RenderObject 側の dirty フラグに基づき
RenderPipelineが差分レイアウト・差分ペイントを実行します。 - 合成フェーズ — レイヤーツリーをクローンしてレンダースレッドへ送ります(Architecture 参照)。
BuildOwner と dirty list
Section titled “BuildOwner と dirty list”BuildOwner(src/FloatSoda/Elements/BuildOwner.cs)は Element の再ビルドをスケジュールする中枢です。WidgetBinding が 1 つ保持し、ルート Element の Mount 時にツリー全体へ伝播します。
MarkNeedsBuild → ScheduledBuildFor
Section titled “MarkNeedsBuild → ScheduledBuildFor”Element を再ビルドしたいときは Element.MarkNeedsBuild() を呼びます。
public void MarkNeedsBuild(){ if (Dirty) return;
Dirty = true; Owner?.ScheduledBuildFor(this);}BuildOwner.ScheduledBuildFor() は Element を dirty list に追加し、初回であれば onBuildScheduled コールバック(WidgetBinding.EnsureVisualUpdate)を発火して「このフレームは描画が必要」というフラグを立てます。
BuildScope の再ビルドループ
Section titled “BuildScope の再ビルドループ”BuildScope() は dirty list を Depth 昇順(親が先) にソートしてから順に Rebuild() します。親を先にビルドするのは、親の再ビルドで子も更新される場合に子の個別ビルドを無駄にしないためです(Flutter と同じ戦略)。
ビルド中に新たな Element が dirty になった場合(ビルド中の MarkNeedsBuild)はリストを再ソートしてループを継続します。ループ終了後に InDirtyList フラグをクリアして dirty list を空にします。
Element は IComparable<Element> を実装しており、Depth → Dirty の順で比較されます。
Element の再ビルドと UpdateChild
Section titled “Element の再ビルドと UpdateChild”再ビルドの実体は各 Element の PerformRebuild() です。
ComponentElement(StatelessElement など)
Section titled “ComponentElement(StatelessElement など)”public override void PerformRebuild(){ var built = Build(); // StatelessWidget.Build(this) を呼ぶ Dirty = false; Child = UpdateChild(Child, built);}UpdateChild(child, newWidget) は Widget の差分を Element ツリーに適用する中心的メソッドです。
| 条件 | 動作 |
|---|---|
newWidget == null |
子を DeactivateChild(RenderObject をツリーから切断)して破棄 |
| 子がいない | InflateWidget で新しい Element を作成して Mount |
child.Widget == newWidget(完全一致) |
何もしない(Element を再利用) |
Widget.CanUpdate(old, new) |
child.Update(newWidget) で既存 Element を更新 |
| それ以外 | 子を破棄して InflateWidget で作り直し |
CanUpdateの判定:Widget.CanUpdate(old, new)は Flutter と同じく「同じ実行時型かつKeyが等しい」ならtrueを返し、既存 Element を再利用します。その手前にあるchild.Widget == newWidget(record の等値比較)は完全一致を素通しする高速パスで、ここで一致すればUpdateすら呼びません。プロパティだけが変わった同型・同 Key の Widget は Element を再利用してプロパティ差分だけが適用されます。Key(ValueKey<T>/UniqueKey)はWidget.Keyとして差分判定に組み込み済みです。
RenderObjectElement
Section titled “RenderObjectElement”RenderObjectElement<T> は Mount 時に CreateRenderObject() で RenderObject を生成し、AttachRenderObject() で最も近い祖先 RenderObjectElement の RenderObject に挿入します(Widget ツリー上では StatelessWidget などレンダリングを伴わない Element を挟めるため、探索が必要です)。
更新時は PerformRebuild() が Widget.UpdateRenderObject(renderObject) を呼び、既存の RenderObject のプロパティだけを書き換えます。プロパティのセッターが MarkNeedsLayout() / MarkNeedsPaint() を呼ぶことで、RenderObject 側の差分更新(RenderObjects)につながります。
Widget が変わる → Element.Update → UpdateRenderObject → RenderObject のプロパティ変更 → MarkNeedsLayout / MarkNeedsPaint → RenderPipeline の dirty list へ → FlushLayout / FlushPaint(変わった部分だけ)ルートの接続: RenderObjectToWidgetAdapter
Section titled “ルートの接続: RenderObjectToWidgetAdapter”Widget ツリーのルートは RenderObjectToWidgetAdapter(Widget)と RenderObjectToWidgetElement<RenderView>(Element)のペアが RenderView に橋渡しします。
RenderViewElement = new RenderObjectToWidgetAdapter { Child = rootWidget, Container = Pipeline.RenderView } .AttachToRenderTree(BuildOwner, RenderViewElement as RenderObjectToWidgetElement<RenderView>);AttachToRenderTree(owner, element) の動作:
- 初回(
element == null) — Element を生成し、owner.BuildScope(() => result.Mount(null))でビルドスコープ内にツリー全体をMountします。 - 2 回目以降 — 既存 Element の
NewWidgetに新しいルート Widget をセットしてMarkNeedsBuild()するだけです。実際の適用は次のBuildScope()内のPerformRebuild()で行われます(ホットリロードやルート差し替えに対応)。
WidgetBinding.DrawFrame
Section titled “WidgetBinding.DrawFrame”WidgetBinding(src/FloatSoda/Core/WidgetBinding.cs)はウィンドウ(オーバーレイ)ごとの調整役です。
public void DrawFrame(){ if (RenderViewElement != null) { BuildOwner.BuildScope(); // 1. dirty Element の再ビルド }
if (!NeedsVisualUpdate || Window == null) return; // 変更がなければ何もしない NeedsVisualUpdate = false;
Pipeline?.FlushLayout(); // 2. 差分レイアウト PostResizeIfSizeChanged(); // レイアウト結果にオーバーレイサイズを追従 Pipeline?.FlushPaint(); // 3. 差分ペイント
if (Pipeline?.RenderView.Layer?.Clone() is not ContainerLayer layer) return;
RenderThreadRunner?.PostRender(Window, layer); // 4. レンダースレッドへ (RenderPostTaskRunner)}NeedsVisualUpdate は次のいずれかで立ちます。
BuildOwnerがビルドをスケジュールしたとき(onBuildScheduled)- RenderObject が
MarkNeedsLayout/MarkNeedsPaintしたとき(RenderPipeline.OnNeedVisualUpdate)
つまり Widget にも RenderObject にも変更がないフレームでは、レイアウト・ペイント・合成のすべてがスキップされます。
未実装の領域
Section titled “未実装の領域”| 対象 | 現状 |
|---|---|
| スクロール系ウィジェット | ListView / GridView / SingleChildScrollView は internal で公開 API から除外。viewport 基盤とあわせて Phase 3 で実装 |
| 画像・アイコン | 描画系の Paint.Image / Paint.Icon を使用可能。フォントは FontProvider 経由で解決 |
| ポインタ入力源 | ヒットテストとジェスチャ認識は実装済み。ただし座標の供給元がダッシュボードオーバーレイにしか接続されておらず、WorldSpaceWindow / DeviceTrackedWindow では入力が届かない |
UI3層構成(FloatSoda.UI / Cream / FizzyPop) |
Phase 5 の予定。ButtonBase / Button / ButtonStyle の型はあるが GestureDetector へ未配線で、3プロジェクトとも NuGet 未配布 |
FloatSoda.Hooks |
HookWidget / HookElement(R3 の ReactiveProperty による UseState)が部分実装。フレームワークのビルドループとは未統合で、HookExtension の各ヘルパーは NotImplementedException |
- Architecture — フレーム全体の流れとスレッドモデル
- WidgetSystem — Widget / Element の使い方と組み込みウィジェット
- RenderObjects — RenderObject 側の差分レイアウト・差分ペイント