実装

画面はファイルだ — 四行から始めるUIランタイム

著者: makemind · 2026年5月13日

クライアント講座 第4回で画面が届いた。ui://desk を読むと546バイトが来て、クライアントはそれをただの文字列として受け取り、長さを数えて終わった。

このトラックはその文字列が絵になる側だ。

四行

{ "type": "page", "title": "Step 1",
  "content": { "type": "center",
    "child": { "type": "text", "text": "The whole screen is this file",
               "style": { "fontSize": 34, "fontWeight": "bold", "color": "#111827" } } } }

233バイト。これが画面のすべてだ。ノードが三つある。

  • page — ひとつの画面。タイトルを持つ。
  • center — ひとつの子を中央に置く。
  • text — 文字。

木だ。page の中に content がひとつ、center の中に child がひとつ。複数を抱えるノードは第2回で出てくる。

これを描く側

final rt = MCPUIRuntime(enableDebugMode: false);
// screen = jsonDecode(ファイルの中身)
await tester.runAsync(() => rt.initialize(screen));
await tester.pumpWidget(RepaintBoundary(
  key: key,
  child: MaterialApp(
    debugShowCheckedModeBanner: false,
    home: Builder(
      builder: (c) => rt.buildUI(
        context: c,
        onToolCall: (tool, params) async => fired.add(tool),
      ),
    ),
  ),
));

initialize にマップを渡し、buildUI がウィジェットを返す。その間に画面についての判断はない。

このホストがテストの中で動くのはキャプチャを撮るためだ。アプリなら pumpWidget の位置に runApp が来る — 前後の二行はそのままだ。

onToolCall は第4回で使う。今は名前を書き取るだけだ。

何が無いかが要点だ

このトラックのホストは host/test/capture_test.dart というファイル一つ。五回が終わるまでほとんど変わらない — 五つの画面すべてを同じホストが描く。

だからこの講座でいちばん大事な検査は「有るか」ではなく 「無いか」 だ。

# 画面に見える文字がプログラムの中にあれば失敗する。
grep -rq 'The whole screen is this file' $SRC && \
  die "step1: the words on the screen are inside the host"

# ウィジェットを手で組んでいれば失敗する。
grep -rqE '\bText\(|\bColumn\(|\bRow\(|\bElevatedButton\(' $SRC && \
  die "step1: the host builds widgets by hand"

$SRChost/test host/pubspec.yaml だ。host/build は外した — コンパイル成果物に文字列がそのまま埋まっていて、そこまで舐めるとこの検査は永遠に通ってしまう。実際、第4回の検査を最初に付けたときそれで誤検知した。

このコンテンツは開発者以上が必要です

サインインしてプランをアップグレードすると続きを読めます。

プランを見る
Twitter