Todo
A Slint UI driven through one generated typed API (TodoApp), running on
either backend: interpreter (runtime compilation via slint_interpreter)
or compiled (FFI bindings to precompiled Rust).
The Rust crates are built automatically by each package's hook/build.dart
(Dart native assets / code assets) — no manual cargo build, no dylib paths.
In release/profile builds the app's hook/link.dart then links the AOT dylib
from the staticlib the build hook produced, tree-shaking components no
reachable Dart code uses (@RecordUse recordings). Recording sits behind a
Flutter experiment flag:
FLUTTER_RECORD_USE=true flutter build macos --release
todo.slint deliberately exports an UnusedGadget component nothing
references: with the flag on, its slint_aot_unused_gadget_* symbols are
absent from the shipped slint_dart_aot dylib; without it (or with
recorded_uses unavailable) every component is kept.
Codegen
cd examples/todo && dart run build_runner build
The .slint lives in ui/, outside lib/: it is not Dart and must never
ship, and build_runner only looks there because build.yaml lists ui/**
as a source.
ui/todo.slint holds only the window: it imports TodoItem and TodoView
(the input, the list and its buttons) from
examples/todo_shared/ui/todo_view.slint, the list UI examples/todo_skia
shows too, and forwards todo-model and the three callbacks. Two files land
in lib/, both regenerated after editing either .slint:
lib/todo.g.dart—slint_generator: the typedTodoApp(properties, callbacks, render target), theTodoItemvalue class generated from the.slintstruct, plus the embedded source and, asslintFiles, thetodo_view.slintit imports. Backend-agnostic —main.dartkeeps the list in the sharedTodoStore(examples/todo_shared) and maps it toTodoItemonly at the Slint boundary, never touching a raw map.lib/todo.aot.g.dart—slint_compiler: the@Nativeexterns andtodoAppFactoryfor the AOT backend.
Backends
main.dart names the UI by its .slint path and no backend at all:
TodoApp.register(); // once, in main()
final TodoApp app = SlintComponent.load('ui/todo.slint'); // synchronous
Registration is per component on purpose: UnusedGadget is never
registered, so nothing references its AOT factory and it is tree-shaken out
of the release dylib. TodoApp.load('ui/todo.slint') is the same thing
without the registry.
The generated TodoApp.defaultFactory picks one by build mode, matching the
dylib that actually ships (the hooks read linkingEnabled, true exactly for
the non-debug modes). Pass a factory to create() to override it — that is
what the tests do. There is nothing to await: compiling with the interpreter
and creating an AOT instance are each one FFI call, so the component is
renderable on return and initState needs no loading state.
Neither mode bundles or reads ui/todo.slint. Debug compiles the copy
todo.g.dart embeds; release compiles it into the AOT dylib, and the Dart
snapshot carries no copy of the text — the embedded string is referenced
only inside the interpreter branch that the const build-mode split makes
dead code. That is also why pubspec.yaml declares no flutter: assets:
entry: Flutter declares assets per package, not per build mode, so anything
listed there for debug convenience would ride along into release.
The import resolves in both modes: the interpreter factory writes the
embedded source and slintFiles into a temporary tree and compiles there,
and the AOT build compiles ui/todo.slint at its absolute path, with the
shared file as a hook dependency. There is no other way in: a .slint is
only ever loaded through its generated wrapper, and the app reaches Slint
only through its typed members (todoModel, onAddTodo, ...).
Interpreter — debug builds
Compiles the source embedded in todo.g.dart at runtime and renders via
slint_interpreter. No runtime asset. The bundle ships only
slint_interpreter_ffi.
cd examples/todo && mise exec -- flutter run
Shared state
The list itself lives in examples/todo_shared: TodoStore owns the
add/toggle/remove-done rules (trim-on-add, ignore-empty, bounds-checked
toggle), the seed list, and the AppBar title helper; TodoPageStateMixin
owns the callbacks, sync and Scaffold. main.dart only loads the component
and maps TodoStore.items to the generated TodoItem in pushTodos — the
same store, page and list UI examples/todo_skia uses, so a change lands in
both apps at once.
Compiled — release/profile builds
Binds straight to the AOT-compiled component, no interpreter. The bundle
ships only slint_dart_aot.
cd examples/todo && mise exec -- flutter run --release
Platforms
macOS, iOS, and Android all build and run. Each platform's Rust cross-compile is driven by the same hooks — the target triple, the NDK clang wrapper on Android, and the deployment target on Apple platforms come from the build input, so there is nothing per-platform to configure:
mise exec -- flutter build macos --release
mise exec -- flutter build ios --release --no-codesign
mise exec -- flutter build apk --release --target-platform android-arm64
Component tree-shaking works on every one: the link hook uses -dead_strip
with an exported-symbols list on Apple platforms and --gc-sections with a
version script on Android, and slint_aot_unused_gadget_* is absent from the
shipped library in all three.
Measured sizes
One .slint file, one screen, arm64 unless noted. Native library is the
Slint code asset the bundle ships — the whole difference between the two
backends:
| Build | macOS | iOS | Android |
|---|---|---|---|
| Release app (AOT) | 58.5 MB | 23.9 MB | 25.3 MB (APK) |
↳ slint_dart_aot |
20.1 MB (universal, 2 slices) | 9.7 MB | 9.9 MB |
| Debug app (interpreter) | 134.7 MB | 127.1 MB | 96.8 MB (APK) |
↳ slint_interpreter_ffi |
26.9 MB | 27.1 MB | 19.6 MB |
The AOT library is roughly 2.8× smaller than the interpreter one, because it
carries compiled components instead of the .slint compiler. The rest of
each app is Flutter itself: Flutter.framework/libflutter.so is 10–39 MB
depending on platform and mode, and debug builds add an unstripped Dart
kernel plus — on Android — a 15 MB Vulkan validation layer.
macOS is the outlier because flutter build macos produces a universal
binary; the per-architecture figure is about half.
Test
Both backends are tested:
cd examples/todo && mise exec -- flutter test
All of them go through the generated TodoApp; none names a Slint property
or callback.
test/todo_typed_test.dart— the generatedTodoAppover the interpreter factorytest/todo_smoke_test.dart— the interpreter engine compilingui/todo.slintat its real path, the raw instance wrapped inTodoApptest/todo_load_test.dart—SlintComponent.loadand per-component registrationtest/todo_patrol_test.dart— end to end through the realTodoPagewithslint_patrol: typing and tapping on the sharedTodoView(TodoView::edit,Add, the checkbox,Remove done items), state read back throughTodoApp($.slintComponent())test/codegen_up_to_date_test.dart— the embedded source andtodo_view.slintcopy still match the files on disktest/todo_compiled_smoke_test.dart— the sameTodoAppover the AOT factory; self-skips underflutter test(always debug, so the AOT dylib isn't built) and runs when the AOT asset is present.flutter build macos --releasestill verifies the AOT codegen/compile/link end to end.
Rust debug/release
The Rust crates build with cargo's release profile by default (debug Slint
rendering is unusably slow). To build them in debug, set the profile
user-define in the pubspec that the hook runner reads — the workspace root
pubspec.yaml in this repo (for a standalone app it would be the app's own):
hooks:
user_defines:
slint_interpreter:
profile: debug
This cargo profile is independent of Flutter's --debug/--release; the
Flutter mode only decides which crate is built (debug → interpreter,
release/profile → AOT).