クライアント講座 第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"
$SRC は host/test host/pubspec.yaml だ。host/build は外した — コンパイル成果物に文字列がそのまま埋まっていて、そこまで舐めるとこの検査は永遠に通ってしまう。実際、第4回の検査を最初に付けたときそれで誤検知した。