Practical Lessons from Upgrading a Large Android Codebase to AGP 8.2.2
This article details a three‑month, multi‑step migration of a large Android monorepo from AGP 7.2.2 to 8.2.2, covering Transform API deprecation, namespace handling, Gradle and JDK upgrades, build‑feature toggles, third‑party library adaptations, performance optimizations, and the troubleshooting of numerous compilation pitfalls.
Introduction
In 2024 I am still doing native Android development without KMP or any "leading‑edge" tools. The current upgrade of the Android Gradle Plugin (AGP) from 7.2.2 to 8.2.2 spans multiple apps and business modules, lasting three months and proceeding in three major phases with many smaller steps.
Upgrade Plan and Pitfalls
The migration moves from 7.2.2 to 8.2.2 . The biggest change in AGP is the deprecation of the Transform Api and the activation of default compilation features. Gradle version and DSL also change significantly, so the upgrade is split into three incremental steps:
Step 1: Upgrade to 7.4.2 to ensure basic compilation works and migrate all Transform interfaces.
Step 2: Upgrade to 8.2.2 , resolve compatibility issues, and collect data on APK size, build time, and R8 compatibility.
Step 3: Fine‑tune features and address any regression in build metrics.
Transform Api
We first identified plugins that still use the Transform Api (four in total, three internal and one Huawei push) and scheduled their migration.
afterEvaluate {
if (project.plugins.hasPlugin('com.android.application')) {
project.gradle.buildFinished {
def app = project.extensions.getByType(com.android.build.gradle.AppExtension.class)
logger.quiet("buildFinished, project(${project.name}) has ${app.transforms.size()} transforms")
app.transforms.forEach {
logger.quiet("transforms: ${it.name} -> ${it.getClass().name}")
}
}
}
}Namespace
AGP 8 requires enabling namespace . A script can strip the old package from XML and add the new namespace declaration in build.gradle . This step can be released independently and is a prerequisite for enabling nonTransitiveRClass .
BuildConfig
By default AGP 8 does not generate BuildConfig . The feature is globally disabled and can be turned on per‑module when needed.
android.defaults.buildfeatures.buildconfig=true
buildFeatures { buildConfig = true }nonFinalResIds
AGP 8 enables nonFinalResIds globally; it cannot be enabled per‑module. In our monorepo we had to disable it for a module that still used butterknife and later re‑enable it after migrating to databinding :
./gradlew app:assembleRelease -Pandroid.nonFinalResIds=falseJVM‑related Changes
AGP 8 requires JDK 17 and updates the default Gradle target compatibility. The following snippet sets Java 1.8 compatibility for existing code while configuring Kotlin to target JVM 1.8:
allprojects {
if (project.plugins.hasPlugin('com.android.application') || project.plugins.hasPlugin('com.android.library')) {
android.compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
if (project.plugins.hasPlugin("kotlin-android")) {
android.kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += ['-Xjvm-default=all']
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += ['-Xjvm-default=all']
}
}
}Gradle Upgrade
Gradle was upgraded from 7 to 8, requiring updates to plugins such as unit‑test, lint, and jcocoa. The official upgrade guide (https://docs.gradle.org/current/userguide/upgrading_version_7.html) was consulted. A Mac‑specific tool‑chain issue was also discovered.
// settings.gradle
buildscript {
dependencies {
classpath "org.gradle.toolchains:foojay-resolver:${foojay_version}"
}
}
apply plugin: "org.gradle.toolchains.foojay-resolver-convention"Third‑Party Libraries
Most third‑party libraries already support AGP 8, but a few (e.g., greendao ) required special adaptation:
// https://github.com/greenrobot/greenDAO/issues/1110#issuecomment-1734949669
tasks.configureEach { task ->
if (task.name.matches("\\w*compile\\w*Kotlin")) {
task.dependsOn('greendao')
}
if (task.name.matches("\\w*kaptGenerateStubs\\w*Kotlin")) {
task.dependsOn('greendao')
}
if (task.name.matches("\\w*kapt\\w*Kotlin")) {
task.dependsOn('greendao')
}
}Okio 1.x was upgraded to 3.x, causing deprecation errors that were suppressed with @Suppress("DEPRECATION_ERROR") and custom lint checks.
@Deprecated(
message = "moved to extension function",
replaceWith = ReplaceWith(
expression = "file.sink()",
imports = ["okio.sink"]
),
level = DeprecationLevel.ERROR,
)
fun sink(file: File) = file.sink() // Temporary suppression of okio compilation error
@Suppress("DEPRECATION_ERROR")Further Transform Details
A simple Transform example registers a task bound to the Variant lifecycle:
val ac = project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
ac.onVariants { variant ->
if (extension.enableTfTask()) {
val taskProvider = project.tasks.register
("${variant.name}TfNothing")
taskProvider.configure { it.extension = extension }
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(taskProvider)
.toTransform(
ScopedArtifact.CLASSES,
TfNothingTask::allJars,
TfNothingTask::allDirectories,
TfNothingTask::output
)
}
}
abstract class TfNothingTask : DefaultTask() {
@get:InputFiles abstract val allJars: ListProperty
@get:InputFiles abstract val allDirectories: ListProperty
@get:OutputFile abstract val output: RegularFileProperty
@Internal lateinit var extension: TfNothingExtension
@TaskAction fun taskAction() { }
}When multiple Transforms are defined, only the last output jar is populated, which can cause full task re‑execution and loss of incremental builds. To mitigate this, we split the large jar into smaller chunks, apply DexBuilder on each chunk in parallel, and merge the results in groups, drastically reducing build time.
val jos = JarOutputStream(FileOutputStream(tmpOutput).buffered())
jos.setLevel(Deflater.NO_COMPRESSION) fun addEntry(jos: JarOutputStream, name: String, data: ByteArray) {
val entry = JarEntry(name)
entry.time = 0
jos.putNextEntry(entry)
jos.write(data)
jos.closeEntry()
}Enabling Partial Compilation Features
nonTransitiveRClass
Because the project is huge, the official migration assistant cannot be used. We wrote a custom plugin that collects each module's packageName or namespace from generated R‑def.txt / R.jar files, aggregates them into a JSON, and then replaces R imports via PSI.
// color.json
{
"auto_night_shade": ["com.xxx.lib.widget"],
"avatar_color_transparent": ["com.xxx.lib.widget", "com.xxx.lib.accountsui"]
}
// id.json
{
"abTestResultTv": ["com.xxx.gripper.app"],
"anr_btn": ["com.xx.gripper.app"]
} // Replace R in Java
private fun replaceExpression(expression: PsiReferenceExpression, s: String) {
val e2 = psiFactory.createReferenceFromText(s, null)
expression.replace(e2)
logger.println("replaceExpression = $expression, newExpression = $s")
}
// Replace R in Kotlin
private fun replaceExpression(expression: KtDotQualifiedExpression, s: String) {
val e2 = psiFactory.createExpression(s)
expression.replace(e2)
logger.println("replaceExpression = $expression, newExpression = $s")
}Data‑binding modules require special handling because the R value is an ID, not a raw color.
# before
android:textColor="@{vm.success? @color/color1 : @color/color2}"
# fix 1
android:textColor="@{vm.success? com.xxx.R.color.color1 : com.yyy.R.color.color2}"
# fix 2
android:textColor="@{util.getResColor(vm.success? R.color.color1 : R.color.color2)}"R8 Issues
Full‑mode R8 is currently disabled; we plan to enable it after further testing. Some C++ code calling Java suffers from missing constructors after R8 optimization, which can be avoided by adding the flag -Dcom.android.tools.r8.disableApiModeling=1 :
./gradlew :app:assembleRelease -Dcom.android.tools.r8.disableApiModeling=1Mapping file format changes and the removal of META-INF/**_release.kotlin_module entries required custom packaging rules and a hook on the mergeReleaseJavaResource task.
packaging {
exclude 'META-INF/**_release.kotlin_module'
} // Remove or modify whenTaskAdded logic to avoid missing base.jar
tasks.whenTaskAdded { task ->
// custom handling
}Data Degradation and Mitigation
The three‑month upgrade caused noticeable degradation in compile time and artifact size. Reducing jar compression, using buffered I/O, multithreaded processing, and grouping Dex merges helped flatten the regression. Enabling nonTransitiveRClass also reduced debug APK size from ~250 MB to ~200 MB.
Conclusion
Despite many twists and challenges, the migration to AGP 8.2.2 was completed successfully, demonstrating that a systematic, incremental approach combined with targeted tooling can overcome the complexities of large‑scale Android build upgrades.
Appendix
Plugin updates: https://developer.android.com/build/releases/gradle-plugin-api-updates?hl=zh-cn
AGP 8.2.0 release notes: https://developer.android.com/build/releases/gradle-plugin?hl=zh-cn
AGP 8.1.0 release notes: https://developer.android.com/build/releases/past-releases/agp-8-1-0-release-notes?hl=zh-cn
AGP 8.0.0 release notes: https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes?hl=zh-cn
AGP 7.4.0 release notes: https://developer.android.com/build/releases/past-releases/agp-7-4-0-release-notes?hl=zh-cn
AGP 7.3.0 release notes: https://developer.android.com/build/releases/past-releases/agp-7-3-0-release-notes?hl=zh-cn
D8 and Kotlin support: https://developer.android.com/build/kotlin-support?hl=zh-cn
Gradle 7 & 8 upgrade guide: https://docs.gradle.org/current/userguide/upgrading_version_7.html
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.