How it works
The host, the bundle, and the loader — and what happens between publish and run.
The Introduction gives the short version: Flitz splits your app into a
host and a bundle, and flitz publish gets a fresh bundle onto a
teammate's device in seconds. This page is the long version. Every capability
Flitz has, and every limit it has, follows from that one split — so it is worth
knowing exactly what sits on each side of the line.
Experimental
Flitz is built on Dart Dynamic Modules (DDM), an upstream-experimental capability of the Dart VM that lets compiled and interpreted Dart code run together in one process. Flitz did not invent a parallel runtime; it uses a real Dart and Flutter mechanism. "Experimental" is the honest label for where that mechanism is today.
Host and bundle
A normal Flutter build compiles everything — your code, the Flutter framework, the Dart platform libraries, the engine, your native plugins — into one artifact. Flitz draws a line through that artifact. Everything heavy or native goes on one side and is built once; everything that changes from commit to commit goes on the other and is published as often as you like.
The host: your app + the loader
The host is your own app with the Flitz loader embedded. It is not a generic container: it is a normal, signed build of your app, produced once from the Flitz SDK your project pins, and installed on a device the way any build of your app is. It carries:
- The Flitz loader — the Android or iOS library you embed. It fetches a bundle, checks that it fits, and runs it.
- The Dart VM with a bytecode interpreter — the same VM Flutter always uses, built with DDM switched on so it can also interpret Kernel Bytecode (KBC) alongside compiled code.
- The Dart platform libraries —
dart:core,dart:async,dart:ui,dart:io,dart:ffiand the rest — compiled ahead of time (AOT) to machine code, in a snapshot that contains no application code. That is what lets one host serve many bundles. - The Flutter engine — rendering, layout, text, platform channels.
- Your native plugins and FFI libraries — compiled in at build time, exactly as in a normal build.
- Your platform code — the Android
Activityor iOS view controller that presents the Flutter view.
Because the host is your app, it inherits your native dependencies. That one fact does a lot of work later on: it is why a bundle can use every plugin you already have, and why it can use only those.
The bundle: a complete build of your app's Dart side
The bundle is what flitz publish produces — a .flitz archive (a ZIP)
containing:
- Your application code, compiled to KBC bytecode instead of machine code.
- The Flutter framework (
package:flutterand everything it pulls in), also as bytecode. A bundle carries the full framework, not a reduced subset — what runs is real Flutter. flutter_assets/— images, fonts, shaders, and the asset manifest. The standard Flutter asset pipeline, unchanged in shape.manifest.json— a small file stating what this bundle needs from a host: the platform it was built against, the plugins and native libraries it uses, and its entry point. The loader reads it before running anything.
Two things a bundle is not. It is not a patch or diff against some released version — it is the whole app, which is why any branch, commit, or experiment can be a bundle, with no dependency on what was published before it. And it contains no native code — nothing the host does not already have.
The trade in one sentence
Portable bytecode on one side; the runtime, engine, and native code on the other, compiled once and shared by every bundle. Swapping bundles swaps your app's code and assets while the runtime stays put.
From publish to run
The workflow has three steps. The first happens once; the other two are the everyday loop.
Embed the loader — once
Add the Flitz loader plugin to your Android and iOS projects, build the host with the Flitz SDK your project pins, and install it on the devices your team reviews on. The host must be a profile or release build — DDM is not available in debug builds.
You rebuild the host only when its native side changes: a new plugin, a new native library, or a new SDK pin. Day-to-day Dart changes never require it.
See Loader plugins for the Android and iOS setup, or Getting started for the end-to-end walkthrough.
Publish a bundle
In your project directory, run flitz publish. It compiles your app and
uploads the result in one go:
flutter build bundlecompiles your Dart code to a kernel file and assemblesflutter_assets/, with the generated plugin registrant included — so plugin wiring survives into the bundle.dart2bytecodecompiles that kernel to KBC bytecode and validates it against the dynamic interface: the declared list of platform APIs that bundle code may call, extend, or override. Code that reaches outside that boundary fails the build on your machine, not at run time on a device.- The manifest is written, recording the platform fingerprint, required plugins, required native libraries, and the entry point.
- The archive is streamed straight to Flitz hosting — it is never written to your disk.
When it finishes, flitz publish prints a link to a small landing page with a
QR code and a flitz://download?url=… deeplink, ready to hand to a teammate.
See flitz publish for flags and machine-readable output.
Open it on a device
A teammate — a designer, product owner, QA, or another developer — scans the QR
code or taps the link on a device that has the host installed. The flitz://
link opens the host, the loader downloads and checks the bundle, and the app
runs. What they see is the real app: full framework, assets, platform channels,
and native plugins, with your latest changes in it.
Laid out end to end:
The two halves of the loop are the uploader (flitz publish, producing a
bundle and a link) and the downloader (the loader inside the host, fetching
and running it). The check in the middle is why a bundle can only run on a host
that was built for it — see The compatibility contract.
Inside the loader
When a flitz:// link opens the host, the loader runs a fixed sequence. Each
step can fail with its own specific error, so a failure names its cause instead
of crashing the app.
- Parse the link.
flitz://download?url=…points at a bundle to fetch;flitz://run?path=…points at a.flitzalready on the device. See URL scheme. - Fetch the bundle — an HTTPS download or a local file read.
- Extract the archive into a per-session temporary directory.
- Validate the manifest against the host — the compatibility check described below. A failing check refuses the bundle before any bytecode is loaded.
- Hand off the bytecode, the assets path, and the entry point to the Flutter engine, started in bytecode mode.
- Clean up the temporary directory when the bundle is torn down.
The loader deliberately distinguishes bundle-side faults (the bundle does not fit this host) from host-side faults (the host build itself is broken), so an error points at whichever side needs fixing.
A load is transient
Running a bundle changes nothing persistent on the device. Restart the app and you are back on whatever the host was built with. There is nothing to roll back and nothing to uninstall.
Mixed-mode execution
Once the loader hands off, the interesting part is how compiled and interpreted code share one process. In plain terms:
- The host starts as it always does: the AOT-compiled Dart VM boots with the platform libraries and the engine.
- The loader gives the engine the bundle's bytecode. The VM loads it directly from the buffer, attaching bytecode to functions without copying it.
- Your
main()— bytecode — runs in the interpreter and callsrunApp(), which is framework code and therefore also bytecode. - Whenever bytecode calls into a platform library — say, the framework calling
dart:uito paint — the VM transitions into compiled code. Whenever compiled code calls back into something you wrote in Dart — an overriddenbuild, a callback, a listener — it transitions back into the interpreter. This is mixed-mode dispatch, and it happens at every call across the boundary. - Rendering, layout, and platform channels run through the normal engine path.
The boundary is invisible to your code. There is no special API, no annotation, and no "Flitz mode" to write against — the same source that compiles to a normal Flutter build compiles to a bundle. The heavy work (rendering, text layout, platform I/O, your native plugins) stays in compiled code; what is interpreted is your app logic and the framework's Dart layer.
There is one Dart VM per process, and it is reused across bundles: switching from one bundle to another tears down the engine instance and its isolates but keeps the VM, so loading a new bundle does not pay the VM start-up cost again.
The compatibility contract
A bundle and a host are compatible when — and only when — they were built against the exact same Dart and Flutter platform. Concretely: the build records a fingerprint of the platform libraries (a SHA-256 hash of the SDK's platform file) in the manifest, and the loader compares it against the fingerprint compiled into the host at build time.
- A match means the bytecode is type-compatible with the host's compiled platform libraries. The bundle runs.
- A mismatch is a hard failure with no fallback. The loader refuses the bundle with a clear error rather than attempting to run it.
Two faster checks back this up, and all three must pass:
| Check | What it guarantees |
|---|---|
| Platform fingerprint matches the host | The bytecode fits the compiled platform libraries in the host. |
| Required plugins are a subset of the host's | Every plugin the bundle calls is compiled into the host. |
| Required native libraries are a subset of the host's | Every FFI library the bundle uses is present in the host. |
In practice this means two things for your team:
- Pin one Flitz SDK and use it on both sides. Build the host with it and
publish with it.
flitz sdkmanages the pin, andflitz statusreports when the two have drifted apart. - Match the build mode. A host built in release mode needs a bundle
published for release (the default); a profile-mode host needs
flitz publish --no-release. The two use different platform builds, so the fingerprint check catches a mismatch. Seeflitz publish.
Because the host's native side is fixed at build time, a bundle runs only on a host built for the same app — one whose plugins and native libraries cover what the bundle needs. Within that envelope, publish as many builds as you like: any branch, any commit, any experiment, without touching the host.
Fits is not the same as safe
The compatibility check guarantees that a bundle fits its host. It does not make a bundle safe. A bundle runs with the full privileges of the host — the same permissions, the same data, the same network access. Flitz is for your own team's bundles: run only bundles you trust as much as your own app's code. Untrusted third-party bundles are not supported.
Constraints
These follow directly from the design above, and they are the questions a sharp evaluator asks first — so here they are up front.
- Experimental. Flitz is built on Dart's upstream-experimental Dynamic Modules. It is leading-edge, not a battle-hardened product yet.
- The host must be a profile or release build. DDM is not enabled in debug builds, so you cannot use a debug build of your app as a host.
- No hot reload, no
dart:mirrors. Flitz does not change your inner loop on your machine; it changes how your team sees work. Hot reload remains your IDE's job. - Host and bundle must share the exact same Flitz SDK. The loader enforces this automatically; you never have to reason about it beyond keeping the pin consistent.
- A bundle cannot add or change native code. It can only use the plugins and FFI libraries the host already has. Needing a new native plugin means a new host build.
- Bundles are for your own app. A host runs bundles of the app it was built for, not arbitrary other apps.
- A load is transient. Nothing persists across a restart; there is nothing to roll back.
- Dev and test only. Flitz never touches your production build, which ships the normal way. Flitz is not code push, not over-the-air updating, and not a way to update a published consumer app — whether that would even be compatible with store policy is an open question, and Flitz makes no claim about it. The clearly supported use is internal: your team, your app, during development.