guide
Build A Complete App
This guide builds a small trace viewer with the production labkit.app contract. The example keeps durable settings in a project, rebuilds transient file data in a session, draws a plot, and exports a result. Each file has one visible responsibility.
File Shape
apps/examples/trace_viewer/
|-- labkit_TraceViewer_app.m
`-- +trace_viewer/
|-- definition.m
|-- projectSpec.m
|-- createSession.m
|-- +workbench/
| |-- buildLayout.m
| `-- present.m
|-- +sourceTrace/
| |-- readTrace.m
| |-- layoutSection.m
| |-- workspacePlot.m
| |-- present.m
| `-- draw.m
`-- +resultFiles/
|-- layoutSection.m
`-- exportTrace.m
There is no handler table, renderer registry, or +userInterface bucket. +workbench is the product assembly boundary. Capability packages own their part of the user workflow.
1. Thin Entrypoint
function varargout = labkit_TraceViewer_app(varargin)
[varargout{1:nargout}] = trace_viewer.definition().launch(varargin{:});
end
The entrypoint does not build state, controls, services, or figures.
2. One App Definition
function app = definition()
app = labkit.app.Definition( ...
Entrypoint="labkit_TraceViewer_app", ...
AppId="examples.trace-viewer", ...
Title="Trace Viewer", ...
Family="Examples", ...
AppVersion="1.0.0", ...
Updated="2026-07-19", ...
Requirements=labkit.contract.requirements("app", ">=1 <2"), ...
ProjectSchema=trace_viewer.projectSpec(), ...
CreateSession=@trace_viewer.createSession, ...
Workbench=trace_viewer.workbench.buildLayout(), ...
PresentWorkbench=@trace_viewer.workbench.present);
end
Definition is a readable inventory, not an execution script. It validates the static layout, direct callback signatures, plot renderers, target IDs, project schema, and presentation contract before native UI mutation.
3. Durable Project
function schema = projectSpec()
schema = labkit.app.project.Schema( ...
Version=1, ...
Create=@createProject, ...
Validate=@validateProject);
end
function project = createProject()
project = struct( ...
"inputs", struct("sources", struct([])), ...
"parameters", struct("gain", 1), ...
"results", struct("lastExport", []));
end
function accepted = validateProject(project)
accepted = isstruct(project) && isscalar(project) && ...
isfield(project, "inputs") && isfield(project.inputs, "sources") && ...
isfield(project, "parameters") && ...
isnumeric(project.parameters.gain) && ...
isscalar(project.parameters.gain) && ...
isfinite(project.parameters.gain);
end
The project contains only durable App meaning. External files are portable source records owned by the runtime, not raw paths.
4. Transient Session
function session = createSession(project, callbackContext)
paths = callbackContext.resolveSourcePaths(project.inputs.sources);
trace = struct("x", [], "y", []);
if ~isempty(paths)
trace = trace_viewer.sourceTrace.readTrace(paths(1));
end
session = struct("trace", trace);
end
createSession(project,callbackContext) is the one reconstruction boundary. The runtime calls it after project restore and file-list source changes. Session data is reconstructible and is not written into the project envelope.
5. Product Layout
function layout = buildLayout()
controls = { ...
trace_viewer.sourceTrace.layoutSection(), ...
trace_viewer.resultFiles.layoutSection()};
workspace = labkit.app.layout.workspace( ...
trace_viewer.sourceTrace.workspacePlot());
layout = labkit.app.layout.workbench( ...
controls, Workspace=workspace);
end
The assembly reads in user order. A complex App can use tabs and workspace pages here without flattening every feature into one file.
The source capability owns its controls:
function section = layoutSection()
section = labkit.app.layout.section("sourceTrace", "Trace", { ...
labkit.app.layout.fileList("traceFiles", ...
Label="Trace file", ...
SelectionMode="single", ...
Bind="project.inputs.sources", ...
SourceRole="trace", ...
SourceIdPrefix="trace"), ...
labkit.app.layout.slider("gain", ...
Label="Gain", Limits=[0.1 10], ...
Bind="project.parameters.gain")});
end
Standard fields and file lists need no App callback. Their bindings update canonical state transactionally.
The result capability binds a real business action directly:
function section = layoutSection()
section = labkit.app.layout.section("resultFiles", "Results", { ...
labkit.app.layout.button("exportTrace", ...
"Export trace", @trace_viewer.resultFiles.exportTrace, ...
Tooltip="Export the calibrated trace and its sampling metadata.")});
end
6. Complete View Snapshot
function view = present(applicationState)
view = labkit.app.view.Snapshot();
view = view.include(trace_viewer.sourceTrace.present( ...
applicationState.session.trace, ...
applicationState.project.parameters.gain));
view = view.enabled("exportTrace", ...
~isempty(applicationState.session.trace.x));
end
+workbench/present.m is a short assembly boundary. It extracts exact inputs and composes feature fragments with Snapshot.include; it does not perform IO or calculation.
function view = present(trace, gain)
model = struct("x", trace.x, "y", trace.y .* gain);
view = labkit.app.view.Snapshot().renderPlot("tracePlot", model);
end
7. Feature-Owned Renderer
function node = workspacePlot()
node = labkit.app.layout.plotArea( ...
"tracePlot", @trace_viewer.sourceTrace.draw);
end
function draw(axesById, model)
ax = axesById.main;
cla(ax);
plot(ax, model.x, model.y);
xlabel(ax, "Time (s)");
ylabel(ax, "Signal");
grid(ax, "on");
end
The plot area references the concrete renderer. The model carries prepared App meaning; drawing does not read project state or dispatch workflow actions.
8. Direct Business Callback
function applicationState = exportTrace( ...
applicationState, callbackContext)
trace = applicationState.session.trace;
gain = applicationState.project.parameters.gain;
choice = callbackContext.chooseOutputFile( ...
{"*.csv", "CSV files"}, "");
if choice.Cancelled
return;
end
tableValue = table(trace.x(:), trace.y(:) .* gain, ...
VariableNames=["Time", "Signal"]);
writetable(tableValue, choice.Value);
applicationState.project.results.lastExport = string(choice.Value);
end
The callback signature exposes its runtime boundary. For larger exports, delegate table construction and result packaging to functions that accept only the trace, gain, and destination they require.
Validation
Test readers, calculations, project validation, snapshot fragments, renderers, and exports directly with synthetic values. Construct definition() in a headless test to validate the whole static contract. Run the App's bounded hidden-GUI workflow after smaller tests are stable; native dialogs and visual quality still require developer-led interactive validation.
Before merge, update the App version, owning manual, structured component history, generated documentation site, and the final branch validation gates.
Related Documentation
Functions And API
Open a function for exact MATLAB syntax, arguments, outputs, behavior, and source.
| Function | Purpose |
|---|---|
labkit.app.Definition | Compile and launch one immutable App SDK contract. |
labkit.app.layout.button | Add a push button with one explicit pressed callback. |
labkit.app.layout.fileList | Add file selection and portable-source controls. |
labkit.app.layout.plotArea | Add one or more axes rendered by an App-owned renderer. |
labkit.app.layout.section | Arrange related child elements under a visible title. |
labkit.app.layout.slider | Add a bounded numeric slider. |
labkit.app.layout.workbench | Assemble controls and a central workspace into the root layout. |
labkit.app.layout.workspace | Define the App's central working area and optional pages. |
labkit.app.project.Schema | Declare one durable App project contract. |
labkit.app.view.Snapshot | Build one immutable complete visible-state snapshot. |
labkit.contract.requirements | Describe the LabKit API versions required by a caller. |