画面就是文件 — 四行开始的 UI 运行时

5 分钟
目标画面课程第 1 讲。客户端课程第 4 讲里服务器送来的那团东西,究竟是什么。从一个四行的 JSON、和一个绝不往里看的宿主开始。

客户端课程第 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 讲的检查刚接上时,正是因此误报过一次。

空的图也是图

截图由 RepaintBoundary.toImage 得到。画面完全没搭起来也照样出 PNG — 一张全白的。

所以不能只看文件在不在。

for s in step1 step2 step3 step3_alt step4 step5_empty step5_filled; do
  [ -s "captures/$s.png" ] || die "$s: no capture"
  # 没搭起来的画面也会留下 PNG — 空的那种。
  [ "$(wc -c <captures/$s.png)" -gt 5000 ] || die "$s: the capture is blank"
done

step1.png 是 25,196 字节。一块白板会掉到五千以下。

渲染

step1  233 B, 4 lines, type "page" -> step1.png

1000×640 逻辑像素,2 倍。淡紫底色正中,一行粗黑字。

这句话在 capture_test.dart 里哪儿都没有。

自己跑一遍

cd content/sample/course-ui
bash verify.sh
open captures/step1.png

带走的

  1. page → content → child — 画面是一棵树
  2. initialize + buildUI — 宿主的活儿只有这两件
  3. 检查「不在」 — 只有确认画面内容不在程序里,「画面就是文件」才成立

下一讲

一个节点成不了画面。多个该放在哪儿 — 而且不用坐标。

运行示例

makemind-academy/course_ui/
git clone https://github.com/makemind-academy/course_ui
cd course_ui
bash verify.sh
在 GitHub 上打开 →