I've been putting off the AGP 9 upgrade on Nodat for a while. The project was stable, CI was green, and "if it ain't broke" felt like reasonable engineering judgment. Then I ran the AGP Upgrade Assistant and found twelve places where applicationVariants was called, four ProGuard rules that R8 no longer respects by default, and a build.gradle.kts using DSL APIs that no longer exist. Then AGP 9.1 shipped and made class repackaging the default. Then AGP 9.2 made wildcard -keepattributes rules stop matching invisible annotations. Then I upgraded.
The good news: none of these are blockers once you understand what changed. The bad news: there are a lot of them, they span three releases, and the error messages when things go wrong are not always obvious about the root cause. Here's every significant change across the 9.x line, in order of how likely it is to actually stop your build.
1. The Variant API Is Actually Gone Now
This was deprecated for years. In AGP 9.0, the deprecated applicationVariants, libraryVariants, testVariants, and unitTestVariants APIs are removed. So is variantFilter. If your build.gradle.kts uses any of these, the build fails with an unresolved reference — no warning, no graceful fallback.
The replacements have been available since AGP 7.x, but they changed the mental model enough that most teams deferred the migration. Here's what it looks like:
// AGP 8.x — applicationVariants to iterate variants
android {
applicationVariants.all { variant ->
variant.signingConfig.enableV1Signing = false
variant.outputs.all { output ->
output.outputFileName = "myapp-${variant.versionName}.apk"
}
}
}
// AGP 9.x — use androidComponents instead
androidComponents {
onVariants { variant ->
variant.signingConfig.enableV1Signing.set(false)
}
}
The output filename case is worth calling out separately. The old output.outputFileName approach no longer works in AGP 9.x because variant outputs are no longer concrete objects you iterate at configuration time. If you were using this pattern to generate versioned APK filenames, you'll need to rethink it with a custom task or the new artifact API instead.
For variantFilter — which many teams used to disable specific build type / flavor combinations — the replacement is beforeVariants:
// AGP 8.x — disable specific flavor combinations
android {
variantFilter { variant ->
if (variant.buildType.name == "debug" &&
variant.flavors.any { it.name == "prod" }) {
setIgnore(true)
}
}
}
// AGP 9.x
androidComponents {
beforeVariants(
selector()
.withBuildType("debug")
.withFlavor("environment", "prod")
) { variantBuilder ->
variantBuilder.enable = false
}
}
Note ComponentBuilder.enabled (with a 'd') is also removed; the property is enable in AGP 9.x.
2. DSL Interface Changes — The Subtle One
AGP 9.0 finalises the new public DSL interfaces. The legacy BaseExtension and other old concrete classes are hidden. For most app developers this is invisible — your standard android { } block keeps working. Where it breaks is in custom Gradle plugins, build-logic modules, and convention plugin code that reaches into AGP internals via extensions.getByType().
// AGP 8.x — worked but used legacy types
val ext: CommonExtension<*, *, *, *, *, *> =
extensions.getByType(CommonExtension::class)
ext.apply {
defaultConfig {
minSdk { version = release(28) }
}
}
// AGP 9.x — new interfaces, slightly different call sites
val ext: CommonExtension =
extensions.getByType(CommonExtension::class)
ext.apply {
defaultConfig.apply {
minSdk { version = release(28) }
}
}
If you maintain a shared build-logic module (Gradle convention plugins are common in large codebases), audit every place you cast or reference AGP extension types. The AGP Upgrade Assistant will flag most of these.
The other DSL change worth knowing: the dexOptions block is removed entirely. It was a no-op ever since D8 replaced DX, but if you have it in your config it'll now fail the build rather than silently ignore it. Same for PostProcessing block, Wear OS wearApp configurations, and density split APK options — all gone, with App Bundle as the recommended path for the latter.
3. The 14 Property Defaults That Silently Changed Your Build
This is the one that catches the most teams by surprise. AGP 9.0 flipped fourteen Gradle property defaults at once. Most of them are harmless — enabling AndroidX by default was inevitable — but a handful have real build impact:
| Property | Before | AGP 9.0 |
|---|---|---|
| android.proguard.failOnMissingFiles | false | true |
| android.r8.strictFullModeForKeepRules | false | true |
| android.r8.optimizedResourceShrinking | false | true |
| android.enableAppCompileTimeRClass | false | true |
| android.onlyEnableUnitTestForTheTestedBuildType | false | true |
| android.defaults.buildfeatures.resvalues | true | false |
| android.defaults.buildfeatures.shaders | true | false |
| android.newDsl | false | true |
| android.builtInKotlin | false | true |
| android.uniquePackageNames | false | true |
| android.useAndroidx | false | true |
| android.sdk.defaultTargetSdkToCompileSdkIfUnset | false | true |
| android.default.androidx.test.runner | false | true |
| android.dependency.useConstraints | true | false |
The three that will actually hurt:
android.proguard.failOnMissingFiles = true. If your proguardFiles block references a file that doesn't exist — a common artifact of copy-pasted build scripts from half-finished features — the build now fails instead of silently skipping it. Highly likely to catch something in older projects.
android.r8.strictFullModeForKeepRules = true. Strict mode rejects keep rules that reference classes or members that don't exist in the codebase. Old ProGuard files accumulate dead rules over years of refactoring. They were always wrong — they just weren't fatal until now. Run a release build, read the R8 diagnostics, and clean up the dead entries.
android.defaults.buildfeatures.resvalues = false. If any of your library modules use resValue in their build files without explicitly enabling the feature, the generated resource value simply won't be there anymore. This one is silent — no build error, just missing values at runtime. Enable per-module if needed:
android {
buildFeatures {
resValues = true
shaders = true // same story for shader support
}
}
4. The R8 Changes Across 9.0, 9.1, and 9.2
Three significant R8 behavior changes shipped across the AGP 9.x line, each in a different minor version.
AGP 9.0: Kotlin null check handling and source file attributes
R8 now processes Kotlin null checks by default with -processkotlinnullchecks remove_message — it removes the message from null check assertions but keeps the actual check via getClass(). This is a reasonable default: it shrinks string constants without removing safety checks. If you need the old behavior, add this to your ProGuard rules:
# Keep Kotlin null check messages intact
-processkotlinnullchecks keep
# Or remove checks entirely (removes the safety net — be careful)
# -processkotlinnullchecks remove
AGP 9.0 also changes the default SourceFile attribute in compiled classes to r8-map-id-<MAP_ID>. This enables automated stack trace retracing in Logcat using the mapping file ID rather than the filename. If you have tooling that parses stack traces and relies on seeing the source filename, it might need updating. Android Studio's built-in retrace and Firebase Crashlytics both handle this correctly already.
AGP 9.1: Class repackaging is on by default
This is the R8 change that surprised me most. AGP 9.1 enables repackaging of compiled classes into the unnamed (default) package by default — equivalent to implicitly adding -repackageclasses to your rules. The practical effect: class names in stack traces will look different in release builds, and any code that relies on the package structure being preserved at runtime (reflective lookups by package, package-level visibility tricks) will behave differently.
If you need to opt out:
# Add to your proguard-rules.pro
-dontrepackage
Worth verifying explicitly on any app that uses reflection to load classes by package, or libraries that have their own ProGuard rules assuming package structure is preserved.
AGP 9.2: Stricter -keepattributes wildcards
Wildcard patterns in -keepattributes directives no longer match runtime invisible annotations. Specifically:
# These rules NO LONGER keep runtime invisible annotations in AGP 9.2+:
-keepattributes *
-keepattributes *Annotation*
-keepattributes *Invisible*
# If you need them, use explicit rules:
-keepattributes RuntimeInvisibleAnnotations,
RuntimeInvisibleParameterAnnotations,
RuntimeInvisibleTypeAnnotations
Runtime invisible annotations are retained only in bytecode, not accessible via reflection — but they matter to tools like serialization libraries, DI frameworks, and annotation processors that inspect bytecode directly rather than through reflection. Most projects won't need to keep these explicitly. But if you're using a niche library that inspects bytecode and you see it behaving differently after the upgrade, this is the first thing to check.
AGP 9.2 also adds support for negated member name patterns in keep rules, which is a useful addition for precisely excluding test-only members from being kept:
-keepclassmembers class com.example.MyClass {
*** !*ForTesting(...);
}
5. Built-in Kotlin: One Less Plugin, One More Version to Watch
AGP 9.0 includes Kotlin compilation built-in, so you no longer need to explicitly apply the org.jetbrains.kotlin.android plugin in your app module. AGP itself has a runtime dependency on Kotlin Gradle Plugin 2.2.10 and will use that version if you don't specify otherwise.
Where this bites you: if your project pins a different KGP version at the root project level and that version conflicts with what AGP expects, you'll get dependency resolution errors that can be confusing to trace. Check your root build.gradle.kts for explicit KGP version declarations and make sure they're compatible with the bundled version. Using the Kotlin BOM usually resolves this cleanly.
For Kotlin Multiplatform projects, the integration changed more significantly: you can no longer use org.jetbrains.kotlin.multiplatform and com.android.library in the same subproject. JetBrains ships a dedicated Android Gradle Library Plugin for KMP that replaces this combination. If you maintain KMP modules with Android targets, check the JetBrains migration guide for the KMP-specific path before upgrading.
6. Compatibility Requirements Changed at Each Minor Version
Unlike some AGP bumps where the minimum Gradle version stays put across the minor line, 9.x raised the minimum at each release:
- AGP 9.0: requires Gradle 9.1.0, JDK 17, SDK Build Tools 36.0.0
- AGP 9.1: requires Gradle 9.3.1
- AGP 9.2: requires Gradle 9.4.1
If you're jumping straight to 9.2 (which you should — there's no reason to stop at 9.0 or 9.1 at this point), you need Gradle 9.4.1. Gradle 9.x itself has its own configuration cache improvements and deprecations, so budget time for that migration in parallel if you're still on a Gradle 8.x wrapper.
The opt-out window is closing: AGP 9.0 shipped with android.newDsl=false as a temporary escape hatch if incompatible third-party plugins blocked your upgrade. That escape hatch is being removed in AGP 10.0, targeting mid-2026 — which is essentially now. If you're still on 8.x waiting for a "good time," the good time was three months ago. The second best time is before AGP 10.0 makes the migration mandatory.
How to Actually Do This Upgrade
The AGP Upgrade Assistant (accessible from the Android Studio menu: Tools → Android → AGP Upgrade Assistant) handles the majority of the mechanical work — variant API migration, removed DSL properties, updated dependency declarations. Run it first, then read the output carefully before applying. It misses some edge cases, particularly in complex multi-module builds or custom plugin code.
Google also released Android Skills for this migration: an AI-assisted upgrade flow available in the Android CLI. JetBrains ships a separate KMP-flavored version for Kotlin Multiplatform projects. For a large monorepo where running the Upgrade Assistant module-by-module would be tedious, both are worth evaluating.
My recommended sequence:
- Run the Upgrade Assistant and review its proposed changes.
- Do a release build with
--infoto surface R8 diagnostics and ProGuard warnings. - Fix dead ProGuard rules flagged by strict mode (
android.r8.strictFullModeForKeepRules). - Audit any module using
resValueor shader compilation — re-enable explicitly where needed. - Run your Macrobenchmark suite and any instrumented tests against the release build. R8 repackaging (9.1) can affect runtime behavior in ways unit tests won't catch.
- Check your stack trace symbolication pipeline. If you use a custom crash reporter or log parser, verify that retracing against the new map format still works.
Most of these changes are things you should have dealt with years ago — dead ProGuard rules, deprecated variant API usage, missing ProGuard files that were silently ignored. The upgrade forces the cleanup. That's uncomfortable in the short term and healthy in the long term.
No comments yet. Be the first to leave one!