Android
Add the loader to an Android host with the pl.leancode.flitz.loader Gradle plugin.
The Android loader is a Maven AAR (pl.leancode.flitz:loader-android) paired
with a Gradle plugin (pl.leancode.flitz.loader). You apply the Gradle plugin;
it adds the AAR to the variants you choose, generates the build-time assets the
loader validates bundles against, and resolves the AAR's manifest contributions.
You never declare the AAR dependency yourself.
Before you start, make sure your app builds against the pinned Flitz SDK and
that flitz plugins credentials --write has stored the repository token on
this machine (see the overview). In
the snippets below, <maven-repo-url> and <plugin-version> are the values
flitz plugins configure prints for your SDK pin.
Let an agent apply the wiring
flitz plugins skill prints these same additions with the concrete
repository URL and plugin version already substituted, as a prompt you can hand
to a coding agent. The steps below explain what that wiring does.
Add the Maven repository
Add the authenticated repository to both blocks of
android/settings.gradle.kts: pluginManagement resolves the Gradle plugin
marker, dependencyResolutionManagement resolves the AAR the plugin pulls in
transitively. The credentials reference the flitzMavenToken property that
credentials --write stored in ~/.gradle/gradle.properties — nothing secret
lands in a committed file.
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
maven {
url = uri("<maven-repo-url>")
credentials {
username = "token"
password = providers.gradleProperty("flitzMavenToken").get()
}
}
}
plugins {
id("pl.leancode.flitz.loader") version "<plugin-version>"
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("<maven-repo-url>")
credentials {
username = "token"
password = providers.gradleProperty("flitzMavenToken").get()
}
}
}
}The Flutter Gradle plugin injects project-level repositories that take
precedence over the settings-level ones, so add the same repository block to
allprojects in the top-level android/build.gradle.kts as well — otherwise
the AAR does not resolve during the app build:
allprojects {
repositories {
google()
mavenCentral()
maven {
url = uri("<maven-repo-url>")
credentials {
username = "token"
password = providers.gradleProperty("flitzMavenToken").get()
}
}
}
}Apply the Gradle plugin
Apply the plugin unconditionally in android/app/build.gradle.kts, after
com.android.application (the plugin fails configuration if it is applied to
anything other than an Android application module). Do not add an
implementation("pl.leancode.flitz:loader-android:…") line — the plugin adds
the AAR at its own version.
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
id("pl.leancode.flitz.loader")
}Choose the flavors that carry the loader
The loader attaches per product flavor, through the flitz { } block.
Prefer a flavor your project already has (a staging or dev flavor is the
usual choice). If the project has no flavors, add a dimension with a
loader-bearing flavor and a clean one:
android {
// … your existing config …
flavorDimensions += "loader"
productFlavors {
create("withLoader") { dimension = "loader" }
create("withoutLoader") { dimension = "loader" }
}
}
flitz {
// The loader is active ONLY in the flavors listed here.
enabledFlavors = listOf("withLoader")
}enabledFlavors is the only activation control — there is no environment
variable, no build-type selector, and no conditional apply. Its rules:
- Opt-in default. An empty or omitted list activates the loader in no
variant. The build logs one
flitz.loader: applied but inactiveline so an inert plugin is never silent. - Matching is on the variant's combined
flavorName. With one dimension that is the flavor itself (withLoader); with two dimensions it is the camel-cased combination (freeStaging), never a bare dimension value. The build type (debug/profile/release) is never part of the decision. - An unknown name fails the build. An entry that matches no produced
variant aborts configuration with a message listing the bad entries and the
flavors the project actually declares. This also catches "set
enabledFlavorsbut declared no flavors": a project withoutproductFlavorsproduces only the empty flavor name.
An inactive flavor's APK is byte-for-byte the same as one built with the plugin
never applied: no AAR, no loader activities, no CAMERA permission, no
flitz:// intent filter, none of the loader's transitive dependencies. Absence
is the off switch.
Manifest — nothing to edit
For each active variant, the AAR's manifest merges into yours automatically:
pl.leancode.flitz.loader.BundleLoaderActivity— exported,singleTask, with aVIEW+DEFAULT+BROWSABLEintent filter for theflitzscheme.pl.leancode.flitz.loader.BundleRunnerActivity— not exported; hosts the bundle's engine.- The
INTERNET,ACCESS_NETWORK_STATE, andCAMERApermissions, and a non-requiredandroid.hardware.camera.anyfeature.
Your own MainActivity keeps MAIN/LAUNCHER, so a home-screen tap still
opens your app; a flitz:// URL is dispatched straight to
BundleLoaderActivity. The intent filter's scheme comes from the
flitzUrlScheme manifest placeholder, which the Gradle plugin sets from
flitz { urlScheme } (default flitz) — see
Custom schemes.
The plugin also registers two per-variant tasks that run on every build and place generated assets in the APK:
flitzGenerateAvailablePlugins<Variant>writesassets/flitz_loader_available_plugins.jsonfrom.flutter-plugins-dependencies(the fileflutter pub getwrites at the project root).flitzGenerateHostConfig<Variant>writesassets/flitz_loader_host_config.json— the expectedplatform_dill_hashvalues derived from the Flitz SDK atflutter.sdkinlocal.properties(falling back toFLUTTER_ROOT). If that path is not a Flitz SDK, the build fails naming the missing file.
Build and run
Build the loader-bearing flavor in profile or release mode — a debug host rejects bundles — for arm64:
flutter build apk --flavor withLoader --release --target-platform=android-arm64Install it on a device, then open a bundle you already have on hand. The
flitz://run form points at an absolute path the app process can read:
adb push my_bundle.flitz /data/local/tmp/my_bundle.flitz
adb shell am start \
-a android.intent.action.VIEW \
-d "flitz://run?path=/data/local/tmp/my_bundle.flitz" \
<your.application.id>Follow the loader log with adb logcat -s FlitzLoader:* flutter:*. A
successful load reports Platform dill hash verified, Plugin check: … — OK,
Native asset check: … — OK, and finally FlutterView attached.
In day-to-day use nobody types that command: flitz publish prints a QR code
and a flitz://download?url=… link; a teammate scans it on a real device and
the loader downloads the bundle over HTTPS and runs it. See
flitz publish.
Optional: make the scanner the first screen
If a build's only purpose is to preview bundles, promote BundleLoaderActivity
to MAIN/LAUNCHER. Put the override in the loader flavor's source set —
never in src/main/, because the clean flavor has no BundleLoaderActivity to
merge against and its build would fail:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<activity
android:name="pl.leancode.flitz.loader.BundleLoaderActivity"
tools:node="merge">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>Then remove MAIN/LAUNCHER from your own MainActivity in that flavor. In
this mode your Flutter UI is unreachable from a launcher tap; the app opens on
the permission gate and QR scanner.