客户端课程第 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),
),
),
),
));
把 map 交给 initialize,buildUI 返回一个 widget。中间没有关于画面的任何判断。
这个宿主跑在测试里,是为了能截图。在真实应用里,runApp 站在 pumpWidget 的位置 — 上下两行不变。
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"
# 宿主若在手写 widget,就失败。
grep -rqE '\bText\(|\bColumn\(|\bRow\(|\bElevatedButton\(' $SRC && \
die "step1: the host builds widgets by hand"
$SRC 是 host/test host/pubspec.yaml。host/build 被排除了 — 编译产物里原样嵌着这些字符串,扫进去这项检查就永远通过。第 4 讲的检查刚接上时,正是因此误报过一次。