数字不在文件里 — 同一个文件,两张图

5 分钟
目标画面课程第 3 讲。一个 `{{now}}` 就把画面和值分开了。至于是否真的分开,靠把同一个文件在两种状态下各渲染一次来定。

到第 2 讲为止,画面只显示文件里写着的东西。叫号屏这么做,号一变就得重发一份画面文件。

把位置空出来

{ "type": "page", "title": "Step 3",
  "initialState": { "now": "0", "waiting": 0 },
  "content": { "type": "center",
    "child": { "type": "linear", "direction": "vertical", "spacing": 10, "alignment": "center",
      "children": [
        { "type": "text", "text": "NOW SERVING", "style": { "fontSize": 15, "letterSpacing": 5, "color": "#6b7280" } },
        { "type": "text", "text": "{{now}}", "style": { "fontSize": 92, "fontWeight": "bold", "color": "#111827" } },
        { "type": "text", "text": "{{waiting}} waiting", "style": { "fontSize": 20, "color": "#6b7280" } } ] } } }

多了两样。

initialState — 这个画面打开那一刻持有的值,免得一开就看见空白。

{{now}} — 把状态里的 now 放在这儿。可以整格用,也可以像 {{waiting}} waiting 那样嵌在句子里。

把值放进去

像 rt.stateManager.set('now', '42') 这样传键和值,用到那个键的节点就重画。宿主不知道是哪些节点,也不需要知道。

接上客户端课程做的东西,连一个个塞都不必了。

onToolCall: (tool, params) async {
  // 服务器返回的 map 原样倒进去
  final r = await client.callTool(tool, params);
  final state = jsonDecode((r.content.first as TextContent).text)
      as Map<String, dynamic>;
  state.forEach(rt.stateManager.set);
},

服务器课程第 3 讲让工具返回 {"now": ..., "waiting": ...}。那份 map 的键和画面上 {{...}} 的名字一致时,中间不需要转换代码。

怎么判断绑定是真的

写了 {{now}},画面上出现 42,就当成了 — 这是容易犯的错。文件里写死 42,画面上也会出现 42。

所以检查分两路。

# 值若被写死进文件,就失败
grep -qE '"(42|7)"' screens/step3.json && die "step3: the value is baked in"
grep -q '{{now}}' screens/step3.json || die "step3: nothing is bound"

# 一个文件,两种状态。图不动,那个绑定就是装饰。
cmp -s captures/step3.png captures/step3_alt.png && \
  die "step3: the same file rendered the same picture for two different states"

测试台把 step3.json 渲染两次:一次 now: 42, waiting: 3,一次 now: 7, waiting: 0。同一个文件。

step3      587 B, 8 lines, type "page" -> step3.png
step3_alt  587 B, 8 lines, type "page" -> step3_alt.png
step3  one file, two states, two pictures (27577 B vs 24734 B)

两张 PNG 不同。文件只有一个,那让它们不同的就是状态。

检查确实抓得住,这一点验过

全通过的检查什么也证明不了。所以把 {{now}} 换成 42 跑了一次。

   [4/8] step3 — the number is not in the file
   step3: the value is baked in

抓住了。第 4、第 5 讲的检查也各破坏一次,三处都抓住了。

多少东西该放进状态

NOW SERVING 是固定文字。本可以抽成 {{label}} 放进状态。

没有抽。放进状态意味着 每次都得有人去填。 漏填,那块就是空的。不变的文字留在文件里,会少一处可能出错的地方。

判据:决定这个值的一方在画面之外,就是状态;否则就写在文件里。

自己跑一遍

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

带走的

  1. initialState + {{key}} — 空出位置,给它一个名字
  2. stateManager.set — 宿主只知道键和值
  3. 两种状态两张截图 — 图动了,才叫绑定

下一讲

值进来了。接下来是出去的一侧 — 按下按钮时发生了什么。

运行示例

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