Steps to reproduce
flutter create flavor_stale_repro
cd flavor_stale_repro
Add product flavors to android/app/build.gradle.kts (inside the android { } block, before buildTypes):
flavorDimensions += "default"
productFlavors {
create("prod") { dimension = "default" }
create("dev") { dimension = "default" }
}
Then:
# 1. Add a marker to lib/main.dart, e.g. first line of main():
# print('probe-AAAA');
flutter build apk --release --flavor prod # build 1 — correct
# 2. Change the Dart code: probe-AAAA -> probe-BBBB
flutter build apk --release --flavor prod # build 2 — INCREMENTAL, reports success
# 3. Inspect what was actually packaged:
unzip -p build/app/outputs/flutter-apk/app-prod-release.apk lib/arm64-v8a/libapp.so | strings | grep -c probe-BBBB # → 0 (expected 1)
unzip -p build/app/outputs/flutter-apk/app-prod-release.apk lib/arm64-v8a/libapp.so | strings | grep -c probe-AAAA # → 1 (expected 0)
Expected results
Build 2 packages the recompiled Dart AOT snapshot (probe-BBBB).
Actual results
Build 2 reports ✓ Built …app-prod-release.apk but the APK contains the previous build's Dart code (probe-AAAA). Every subsequent incremental --flavor release build keeps shipping the snapshot from the variant's first build until flutter clean.
Where it breaks (verified by content, not timestamps)
The Dart compile chain is healthy; the AGP hand-off is not:
| Stage |
Content after build 2 |
intermediates/flutter/prodRelease/arm64-v8a/app.so (AOT output) |
fresh — contains probe-BBBB |
intermediates/flutter/prodRelease/jniLibs/arm64-v8a/libapp.so (Flutter→AGP hand-off) |
fresh — contains probe-BBBB |
intermediates/merged_jni_libs/prodRelease/mergeProdReleaseJniLibFolders/out/…/libapp.so |
STALE — still probe-AAAA, mtime frozen at build 1 |
| merged_native_libs → stripped_native_libs → APK |
faithfully repackage the stale bytes (with fresh mtimes) |
i.e. :app:mergeProdReleaseJniLibFolders is considered UP-TO-DATE even though the Flutter jniLibs directory it consumes has changed content. Deleting merged_native_libs + stripped_native_libs does NOT help (they re-run but read the frozen merged_jni_libs); deleting merged_jni_libs/<variant> as well produces a correct APK.
Scope / what is NOT affected
- No flavors → not affected. Same machine, same SDKs, plain
flutter build apk --release propagates Dart changes correctly.
- Debug /
flutter run → not affected (kernel ships in flutter_assets, bypassing the jniLibs chain).
- Reproduces on two AGP/Gradle generations: the fresh
flutter create template (Gradle 9.1.0 / AGP 9.0.1 / Kotlin 2.3.20) and an existing app (Gradle 8.14 / AGP 8.11.1 / Kotlin 2.2.20, Groovy DSL).
android.builtInKotlin=false / android.newDsl=false make no difference (tested with and without).
- Still present in 3.44.1. Did not occur before upgrading to 3.44 (same project built correctly on the previous stable).
Why this is nasty
The build exits 0 and prints the success line, so flavored production APKs/AABs silently ship old business logic. Detection requires binary inspection — most users will misattribute it to generic caching and flutter clean it away without reporting.
Workarounds
flutter clean before every release build, or
- delete
build/app/intermediates/{merged_jni_libs,merged_native_libs,stripped_native_libs}/<variant> before building.
flutter doctor -v (summary)
[✓] Flutter (Channel stable, 3.44.1, on macOS 26.5 25F71 darwin-arm64)
• Dart version 3.12.1
[✓] Android toolchain (Android SDK version 36.0.0)
• Java OpenJDK Zulu17.54+21-CA (17.0.13+11-LTS)
[✓] Xcode 26.5
Steps to reproduce
flutter create flavor_stale_repro cd flavor_stale_reproAdd product flavors to
android/app/build.gradle.kts(inside theandroid { }block, beforebuildTypes):Then:
Expected results
Build 2 packages the recompiled Dart AOT snapshot (
probe-BBBB).Actual results
Build 2 reports
✓ Built …app-prod-release.apkbut the APK contains the previous build's Dart code (probe-AAAA). Every subsequent incremental--flavorrelease build keeps shipping the snapshot from the variant's first build untilflutter clean.Where it breaks (verified by content, not timestamps)
The Dart compile chain is healthy; the AGP hand-off is not:
intermediates/flutter/prodRelease/arm64-v8a/app.so(AOT output)probe-BBBBintermediates/flutter/prodRelease/jniLibs/arm64-v8a/libapp.so(Flutter→AGP hand-off)probe-BBBBintermediates/merged_jni_libs/prodRelease/mergeProdReleaseJniLibFolders/out/…/libapp.soprobe-AAAA, mtime frozen at build 1i.e.
:app:mergeProdReleaseJniLibFoldersis considered UP-TO-DATE even though the Flutter jniLibs directory it consumes has changed content. Deletingmerged_native_libs+stripped_native_libsdoes NOT help (they re-run but read the frozenmerged_jni_libs); deletingmerged_jni_libs/<variant>as well produces a correct APK.Scope / what is NOT affected
flutter build apk --releasepropagates Dart changes correctly.flutter run→ not affected (kernel ships in flutter_assets, bypassing the jniLibs chain).flutter createtemplate (Gradle 9.1.0 / AGP 9.0.1 / Kotlin 2.3.20) and an existing app (Gradle 8.14 / AGP 8.11.1 / Kotlin 2.2.20, Groovy DSL).android.builtInKotlin=false/android.newDsl=falsemake no difference (tested with and without).Why this is nasty
The build exits 0 and prints the success line, so flavored production APKs/AABs silently ship old business logic. Detection requires binary inspection — most users will misattribute it to generic caching and
flutter cleanit away without reporting.Workarounds
flutter cleanbefore every release build, orbuild/app/intermediates/{merged_jni_libs,merged_native_libs,stripped_native_libs}/<variant>before building.flutter doctor -v (summary)