Advanced
Virtualized list-view
For large datasets, list-view renders only visible items (plus a buffer)
and recycles widgets on scroll:
delegate "RoomCard" {
hbox {
fill-width #true; padding 8; gap 8; align-items "center"
label { text bind="item.name" }
label { text bind="'' + item.brightness + '%'" }
}
}
list-view "rooms" as="item" delegate="RoomCard" {
item-height 48
buffer 3
fill-width #true; height 300
}| Property | Description |
|---|---|
| first argument | State array key |
as="var" | Loop variable name (default "item") |
delegate="Name" | Item template (auto-receives item and index) |
item-height | Fixed height per item (required for virtualization) |
buffer | Extra items rendered above/below the viewport (default 2) |
Updates are minimal: structural array changes add/remove/rebind slots, per-field changes update only the affected slot, and full replacement rebinds all slots. Items can also be declared inline instead of via a delegate.
Actions inside a delegate can use the loop index:
button {
label "X"
on-click { array-remove "items" expr="index" }
}Timers
// Repeating. Ticks every second
timer {
interval 1000
set "seconds" expr="app.seconds + 1"
}
// One-shot. Fires once after 3 seconds
timer {
interval 3000
repeat #false
set "message" value="Welcome!"
}
// Reactive control. Runs only while app.running is true
timer {
interval 1000
active bind="app.running"
set "seconds" expr="app.seconds + 1"
}Timers accept any action, work inside components (with self. state), and
active bind= turns a stopwatch into three lines of markup.
Commands
Commands are firmware-registered callbacks invocable from the UI. They are the counterpart to state: state flows data down, commands send intents up.
interpreter.commands().register_command("shuffle", [&](const Value&) {
// firmware-side logic
});button {
label "Shuffle"
on-click { cmd "shuffle" }
}Assets
The assets {} block declares the images and fonts a document requires:
assets {
image "bg" source="assets/bg.png"
font "title" source="fonts/Title.ttf" size=32
}
image { src "bg" }
label { text-font "title" }Two validation levels apply: at compile time, literal src / text-font
references are checked against the manifest (with warnings for unknown
names); at load time, each declared asset is checked against what the
firmware actually registered. Missing images render blank and missing fonts
fall back to default. The app never crashes over an asset.
Firmware registers assets by name:
kadokit::register_font_asset("my_font", &my_custom_font);
kadokit::register_image_asset("my_icon", my_icon_data);On desktop builds with filesystem support, image.src can also load PNG,
JPG, BMP, and SVG files directly by path.
Firmware integration
The host API is deliberately small. On desktop the interpreter loads
.ui.kdl sources directly; on device it loads the compiled .bui binaries
(see Getting started → From desktop to device):
using namespace kadokit;
Interpreter interpreter;
interpreter.expose("app");
interpreter.load_file("app.ui.kdl");
// Read/write state
auto& state = interpreter.state();
state.set_from_firmware("temperature", Value::from_int(25));
int brightness = state.get("brightness").as_int();
// Subscribe to changes
state.subscribe("brightness", [](std::string_view key, const Value& val) {
hardware_set_backlight(val.as_int());
});
// Register commands
interpreter.commands().register_command("reboot", [](const Value&) {
hardware_reboot();
});Read-only state
State declared read-only=#true can only be updated through
set_from_firmware(). UI-side writes are rejected as no-ops. Sensor
readings stay authoritative.
Batching
Group updates so the UI refreshes once:
{
StateStore::BatchGuard guard(state);
state.set_from_firmware("temperature", Value::from_int(25));
state.set_from_firmware("humidity", Value::from_int(70));
} // all UI updates fire here, once per changed key
The interpreter also coalesces automatically by default, flushing state changes once per refresh frame.
Hot reload
interpreter.reset(); // clears state, styles, components, screens, JS context
interpreter.expose("app");
interpreter.load_file("app.ui.kdl");Diagnostics
Documents are validated at load time with source-location context:
error: slider > bind-value: unknown state variable 'brighness'; did you mean 'brightness'?
warning: slider > bind-value: binding 'app.name' (string) to slider.value (expected int)
error: Card: missing required prop 'body'Unknown widget types and state variables get suggestions for likely names via edit distance. Type mismatches between bindings and widget expectations are warned about before anything renders.