Flitzdocs
Loader plugins

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.

android/settings.gradle.kts
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:

android/build.gradle.kts
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.

android/app/build.gradle.kts
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/app/build.gradle.kts
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 inactive line 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 enabledFlavors but declared no flavors": a project without productFlavors produces 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 a VIEW + DEFAULT + BROWSABLE intent filter for the flitz scheme.
  • pl.leancode.flitz.loader.BundleRunnerActivity — not exported; hosts the bundle's engine.
  • The INTERNET, ACCESS_NETWORK_STATE, and CAMERA permissions, and a non-required android.hardware.camera.any feature.

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> writes assets/flitz_loader_available_plugins.json from .flutter-plugins-dependencies (the file flutter pub get writes at the project root).
  • flitzGenerateHostConfig<Variant> writes assets/flitz_loader_host_config.json — the expected platform_dill_hash values derived from the Flitz SDK at flutter.sdk in local.properties (falling back to FLUTTER_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-arm64

Install 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:

android/app/src/withLoader/AndroidManifest.xml
<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.

Next steps

On this page