一つのファイル、二つの形 — 数を知らない一覧
5 分ここまでの画面は、値が変わっても形はそのままだった。実際の画面はそうではない — 待ちがいないときと三人のときでは違うものを見せなければならない。
分かれる場所
{ "type": "conditional", "condition": "{{empty}}",
"then": { "type": "text", "text": "Nobody is waiting", ... },
"orElse": { "type": "box", ... } }condition が真なら then、偽なら orElse。どちらも普通のノードなので、何でも入る。
ここで {{empty}} を使ったのが要点だ。{{waiting == 0}} のような式は書いていない。条件を 決める側 は画面ではなくサーバーだ — 「空である」の定義が変われば(たとえば予約待ちは数えないことにすれば)、直すのはサーバーだけになる。
数を知らない行
{ "type": "list", "items": "{{rows}}",
"itemTemplate": { "type": "linear", "direction": "horizontal", "distribution": "spaceBetween",
"children": [
{ "type": "text", "text": "{{item.no}}", ... },
{ "type": "text", "text": "{{item.since}}", ... } ] } }items は配列を指し、itemTemplate は 一行 の形だ。行が何本かはどこにも書いていない。
テンプレートの中では {{item.*}} でその行の値を使う。外の状態と名前が混ざらない。
{{rows}} に三つ入れれば三本、十入れれば十本できる。ファイルは1,234バイトのままだ。
同じファイルを二度レンダーする
この回の主張は「一つのファイルが二つの形になる」だ。キャプチャ一枚では証明にならない。
await render('step5', {'empty': true, 'waiting': 0, 'rows': <dynamic>[]},
as: 'step5_empty', ...);
const rows = [
{'no': '44', 'since': 'waiting 2 min'},
{'no': '45', 'since': 'waiting 5 min'},
{'no': '46', 'since': 'waiting 9 min'},
];
await render('step5', {'empty': false, 'waiting': rows.length, 'rows': rows},
as: 'step5_filled', ...);そして何行が実際に組み上がったかを数える。
final built = [
for (final r in rows)
if (find.text(r['no']!).evaluate().isNotEmpty) r['no']!
];step5_empty 1234 B, 16 lines, type "page" -> step5_empty.png
empty state shows: 1 placeholder, 0 rows
step5_filled 1234 B, 16 lines, type "page" -> step5_filled.png
filled state shows: 0 placeholder, 3 rows (44, 45, 46)数えるのは、数が違っても絵はもっともらしいからだ。三本のうち二本しか描かなくてもカードは埋まって見える。
grep -qE '"(44|45|46)"' screens/step5.json && die "step5: the rows are baked in"
grep -q '"itemTemplate"' screens/step5.json || die "step5: no row template"
cmp -s captures/step5_empty.png captures/step5_filled.png && \
die "step5: the conditional did not change anything"
grep -q 'empty state shows: 1 placeholder, 0 rows' $LOG || \
die "step5: the empty branch is wrong"
grep -q 'filled state shows: 0 placeholder, 3 rows (44, 45, 46)' $LOG || \
die "step5: the list did not build one row per item"{{item.no}} を 44 に変えて回すと、最初の行が捕まる。
五回分
step1 233 B, 4 lines, type "page" -> step1.png
step2 906 B, 12 lines, type "page" -> step2.png
step3 one file, two states, two pictures (27577 B vs 24734 B)
step4 pressed -> the host was asked for: queue.next, queue.take
step5 empty state shows: 1 placeholder, 0 rows
filled state shows: 0 placeholder, 3 rows (44, 45, 46)
live ui://desk — 546 B, 6 lines, type "page", title "Desk"
the server now reports {"waiting":2} after one press
5 steps + the live loop · each checked against its own claim · one host, five screens, no widgets written by hand最後の行がこのトラックの要旨だ。五つの画面が違うことをするのに、描くコードは一つ。第1回以降ホストのレンダー部分は変わっておらず、増えたのは onToolCall の中身と — キャプチャが見られないものを書き取る — 検査用の行だけだ。
三トラックが繋がる場所
course-server 六編 ツールを開き、断り、画面をリソースとして出し、変われば知らせる
course-client 五編 繋ぎ、尋ね、呼び、画面を受け取り、購読する
course-ui 五編 受け取ったその画面が絵になる三つで一つの待合室を扱っている。サーバーの desk.admit がクライアントの callTool になり、サーバーが ui://desk で出したJSONがこのトラックの screens/*.json と同じ文法だ。
それが本当かは最後の検査が確かめる。host/test/live_test.dart がサーバートラックの完成版を実際に起動し、ui://desk を読み — screens/ には無い画面だ — そのままレンダーして、その画面が持っていた Admit one を押す。
ui://desk — 546 B, 6 lines, type "page", title "Desk"
nothing about this screen is on disk here; it arrived just now
opened with 3 waiting
pressed Admit one
the server now reports {"waiting":2}一度押して3が2になった。その前後のキャプチャ二枚が違うことまで検査が見る。サーバーの画面をこのフォルダにコピーすると [ -e screens/desk.json ] で捕まる — コピーを描いたのでは、この回の主張が消えるからだ。
クライアントトラックは模擬サーバーを使わない — 五段階すべてサーバートラックのそのサーバーを実際に起動して繋ぐ。だから二つのトラックがずれれば検査が捕まえる。実際にクライアント第3回で一度捕まり、サーバー側で誤ってまとめられていた断りの文言がそのとき見つかった。
自分で回してみる
cd content/sample/course-ui
bash verify.sh
open captures/step5_empty.png captures/step5_filled.png持ち帰るもの
conditional— 条件は状態で受ける、式で書かないlist+itemTemplate— 一行の形だけを書く- 行数を数える — 数が違っても絵はもっともらしい
次へ
三トラックが扱ったのは一つの待合室だ。来月からはこの画面が、人が実際に立っている場所へ降りる — 倉庫、交代の引き継ぎ、回線の切れたレジ。
サンプルを実行する
makemind-academy/course_ui/git clone https://github.com/makemind-academy/course_ui cd course_ui bash verify.sh