数字はファイルに無い — 同じファイル、二つの絵

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 {
  // サーバーが返したマップをそのまま流し込む
  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": ...} を返すようにしてある。そのマップのキーと画面の {{...}} の名前が同じなら、あいだに変換コードは要らない。

バインディングが本物かを確かめる方法

{{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 で開く →