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 flavors you choose, checks at build time that your Flitz
SDK is new enough for this plugin version, and merges 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 Credentials). In the snippets below,
<plugin-version> is the value flitz plugins configure prints for your SDK
pin.
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("https://dl.cloudsmith.io/basic/leancode/flitz/maven/")
credentials {
username = "token"
password = providers.gradleProperty("flitzMavenToken").get()
}
}
}
plugins {
id("pl.leancode.flitz.loader") version "<plugin-version>"
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("https://dl.cloudsmith.io/basic/leancode/flitz/maven/")
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("https://dl.cloudsmith.io/basic/leancode/flitz/maven/")
credentials {
username = "token"
password = providers.gradleProperty("flitzMavenToken").get()
}
}
}
}Every build needs the token
The plugin is always applied, so every Gradle build of this project resolves
it from the private repository — loader-free flavors and CI included. Without
flitzMavenToken, Gradle fails while reading the settings, before any flavor
is chosen. Run flitz plugins credentials --write on each developer machine.
In CI, supply the token as the Gradle property flitzMavenToken, for example
through the ORG_GRADLE_PROJECT_flitzMavenToken environment variable
(flitz plugins credentials --print-token prints it).
Apply the Gradle plugin and raise minSdk
Apply the plugin unconditionally in android/app/build.gradle.kts. It
works only on the app module — the module that applies
com.android.application — and fails configuration anywhere else. Do not add an
implementation("pl.leancode.flitz:loader-android:…") line — the plugin adds
the AAR at its own version.
The loader requires Android 10 (API 29). The Flutter template's
minSdk = flutter.minSdkVersion is lower, and the manifest merge of a loader
flavor fails until you raise it:
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
id("pl.leancode.flitz.loader")
}
android {
// … your existing config …
defaultConfig {
// … your existing config …
minSdk = 29
}
}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 one flavor
that includes the Flitz loader and one that does not:
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 loader permissions, no
flitz:// intent filter, none of the loader's transitive dependencies. Absence
is the off switch.
Every build of an active flavor also runs an SDK floor check first. It fails the build when the Flutter SDK the build uses is not a Flitz SDK, or is older than this plugin version supports. The message names the fix — see Troubleshooting.
Manifest — nothing to edit
For each active variant, the AAR's manifest merges into yours automatically:
pl.leancode.flitz.loader.BundleLoaderActivity— exported,singleTask; takesflitz://downloadlinks.pl.leancode.flitz.loader.PanelActivity— exported; takesflitz://flitz-panellinks and opens the panel. Without sign-in it closes at once, showing nothing.- A sign-in callback activity that takes
flitz://flitz-authlinks, where the sign-in page returns to the app. Without sign-in it does nothing. pl.leancode.flitz.loader.BundleRunnerActivity— not exported; hosts the bundle's engine.- The
INTERNETandACCESS_NETWORK_STATEpermissions, and a package query that lets the loader open the sign-in page in a Custom Tab.
Your own MainActivity keeps MAIN/LAUNCHER — unless you
launch into the panel — so a
home-screen tap still opens your app. Only those three hosts go to the loader; links on your scheme with any other
host reach your own activities. The scheme comes from flitz { urlScheme }
(default flitz) — see Custom schemes.
To open a download link or the panel from your own Kotlin code, see the
API reference.
Build and run
Build the flavor that includes the Flitz loader in release mode — a debug host cannot run bundles — and install it on an arm64 device or an x86_64 emulator:
flutter build apk --flavor withLoader --release
adb install build/app/outputs/flutter-apk/app-withloader-release.apkThen publish a bundle with flitz publish, open the landing page it prints on
the device, and tap its button. The loader downloads the bundle over HTTPS and
runs it. In a host with sign-in, the first load
opens the sign-in page in the browser first. See flitz publish.
Follow the loader log with adb logcat -s FlitzLoader:* flutter:*. A
successful load logs Platform dill hash verified,
Native package check: … — OK, and finally FlutterView attached.
Where bundles run
All loader activities — the download, sign-in, panel, and bundle screens — run
in a separate :flitz process and task. That is what lets a bundle start at
any point in your app's life, and it has consequences:
- Your app keeps running. It stays in its own task with its state, and Back from a bundle returns to wherever the link was opened from — the browser, for example, or the panel when you ran the build from it — not into your app.
- Your
Applicationclass is created again in the:flitzprocess, and any in-memory singleton exists once per process. - ContentProvider-initialized libraries are not initialized there. Android
creates a
ContentProvideronly in the process that declares it, so SDKs that start themselves from one — Firebase, WorkManager, AndroidX Startup, and similar — are not set up in:flitz. Bundle code that needs them initializes them itself, for example withFirebase.initializeApp. - Leave the process alone. Do not override
android:processon the loader activities, and do not declare aFlutterActivityin the:flitzprocess. The loader refuses to run with "Cannot run bundles in this process".
Plugins register on the bundle's engine automatically, and their native code
runs in the :flitz process.
Next steps
Sign-in and the panel
Enterprise, optional: turn on sign-in and the panel in the same flitz { } block.
URL scheme
What flitz://download accepts, and how to use your own scheme.
Compatibility
What must match between a host and a bundle, and how to fix a mismatch.
Troubleshooting
Host build errors and load-time rejections.