The Screen Is a File — Starting a UI Runtime in Four Lines
5 minIn client part 4 the screen arrived. Reading ui://desk brought back 546 bytes, and the client took it as a string, counted the length, and stopped there.
This track is the other side: how that string becomes a picture.
Four lines
{ "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 bytes. That is the whole screen. Three nodes.
page— one screen, with a title.center— puts its one child in the middle.text— words.
It is a tree. page holds one content, center holds one child. Nodes that hold several children arrive in part 2.
The side that draws it
final rt = MCPUIRuntime(enableDebugMode: false);
// screen = jsonDecode(file contents)
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),
),
),
),
));Hand initialize a map, and buildUI gives back a widget. Nothing in between judges the screen.
This host runs inside a test so it can take captures. In an app, runApp stands where pumpWidget is — the two lines around it are the same.
onToolCall gets used in part 4. For now it only writes the name down.
What matters is what is absent
The host for this track is one file, host/test/capture_test.dart. It barely changes across the five parts — all five screens are drawn by that same host.
So the most important check in this course is not "is it there" but "is it not there".
# Fails if the words on the screen live inside the program.
grep -rq 'The whole screen is this file' $SRC && \
die "step1: the words on the screen are inside the host"
# Fails if the host is building widgets by hand.
grep -rqE '\bText\(|\bColumn\(|\bRow\(|\bElevatedButton\(' $SRC && \
die "step1: the host builds widgets by hand"$SRC is host/test host/pubspec.yaml. host/build is excluded — compiled artifacts quote the strings back at you, and sweeping them in makes this check pass forever. That is exactly how part 4's check misfired the first time it was wired.
A blank picture is still a picture
Captures come out of RepaintBoundary.toImage. A screen that never built still produces a PNG — a white one.
So checking that the file exists is not enough.
for s in step1 step2 step3 step3_alt step4 step5_empty step5_filled; do
[ -s "captures/$s.png" ] || die "$s: no capture"
# A screen that failed to build still produces a PNG — an empty one.
[ "$(wc -c <captures/$s.png)" -gt 5000 ] || die "$s: the capture is blank"
donestep1.png is 25,196 bytes. A white slab falls under five thousand.
The render
step1 233 B, 4 lines, type "page" -> step1.png1000×640 logical pixels at 2×. Bold black text in the middle of a pale lilac field.
That sentence appears nowhere in capture_test.dart.
Run it yourself
cd content/sample/course-ui
bash verify.sh
open captures/step1.pngWhat to take away
page→content→child— a screen is a treeinitialize+buildUI— those two are the host's whole job- Check for absence — "the screen is a file" is only true if the screen's contents are not in the program
Next
One node is not a screen. Next: where to put several of them — without coordinates.
Run the sample
cd course-ui bash verify.sh