Backends

One typed API, two independent backends behind it. The generated wrapper's defaultFactory follows the Flutter build mode, exactly like the hooks that decide which dylib ships — interpreter in debug, AOT in release/profile.

Interpreter (debug)

Compiles the source embedded in foo.g.dart at runtime and renders via slint_interpreter. What the source imports or loads through @image-url is embedded next to it (slintFiles); SlintInterpreterFactory writes both into a temporary tree (writeSlintTree) so the compiler resolves them at a real path. No runtime asset. The bundle ships only slint_interpreter_ffi.

cd examples/todo && mise exec -- flutter run
        

There is no runtime path for a .slint without a generated wrapper: loading a .slint in code always gives back its wrapper, in every build mode. See the interpreter package.

AOT (release/profile)

Binds straight to the AOT-compiled component, no interpreter. The bundle ships only slint_dart_aot.

cd examples/todo && mise exec -- flutter run --release
        

The app's hook/build.dart calls buildSlintAot: slint-build codegen of every ui/**.slint plus generated C ABI glue, built as a staticlib. Each .slint compiles at its absolute path, so its imports resolve, and files it reads from outside ui/ are hook dependencies. The app's hook/link.dart calls linkSlintAot: relinks the final dylib from that staticlib, keeping only the components the app uses. See the AOT package for the pipeline, the release-size table, and the tree-shaking mechanism.

Component tree-shaking works behind Flutter's record-use flag:

FLUTTER_RECORD_USE=true flutter build macos --release
        

Without recordings every component is kept — the deliberate keep-all fallback. The example's UnusedGadget component exists to prove the mechanism: exported and compiled like any other, referenced by no Dart code, and absent from the shipped dylib when the flag is on.

Adding a backend

A backend extends SlintComponentFactory (see the codegen package) and is passed to the wrapper's create() — defaultFactory just picks one from the package's dependencies at codegen time. The two backends never meet: the pick is a const branch, so a release binary carries no interpreter path and a debug run never loads the AOT dylib.

A backend with no factory (slint_skia) still ends in the wrapper: the app compiles through that engine and passes the component to the generated constructor, TodoApp(component).