The Screen Is a File — Starting a UI Runtime in Four Lines

5 min
GoalScreen course, part 1. What exactly was that blob the server sent in client part 4. It starts with one four-line JSON file and one host that never looks inside it.

In 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"
done

step1.png is 25,196 bytes. A white slab falls under five thousand.

The render

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

1000×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.png

What to take away

  1. page → content → child — a screen is a tree
  2. initialize + buildUI — those two are the host's whole job
  3. 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