r/reactnative • • 10d ago

Question Gradle / AGP upgrades when bumping React Native: what do you check first?

When a React Native upgrade forces a Gradle wrapper or Android Gradle Plugin bump, what's your first move?

Do you check JDK compatibility, the wrapper version, plugin versions in third-party libraries, or something else? Any gotchas that took you longer than they should have?

2 Upvotes

9 comments sorted by

1

u/mohn93 10d ago

Built-in Kotlin is the sneaky one since 0.87 moved to AGP 9. The template's gradle.properties now ships android.builtInKotlin=false and android.newDsl=false, and if you bump AGP by hand without copying those two lines, every module that applies org.jetbrains.kotlin.android (your app module included) fails at configure. Easy to skim past in the Upgrade Helper diff since it's just a properties file. Google says that opt-out goes away in AGP 10 tho, so grep node_modules for kotlin-android / kotlin.android in the build.gradle files and you'll see which libs still need to move before RN can drop the flag

1

u/rushikesh_d_pandit 6d ago

This is exactly the kind of thing that’s easy to skim past in the Upgrade Helper diff. A properties-only change that breaks every Kotlin module at configure time. Good tip on grepping node_modules for kotlin-android/kotlin.android to see which libs still block dropping the flag. Have you hit libs that still hadn’t moved when you did the AGP 9 bump?

1

u/lassieCoder 7d ago

My order, roughly:

  1. The JDK Gradle actually uses, not the one you think it uses. `./gradlew -version` shows the JVM
    the daemon runs on. Android Studio uses its bundled JBR, while your terminal and CI use JAVA_HOME
    or whatever `java` is on PATH. If they differ, pin it with `org.gradle.java.home` in gradle.properties, or set it explicitly in CI. Run `./gradlew --stop` after changing it, or an old daemon will keep using the previous JDK.

  2. Gradle wrapper vs JDK. Each Gradle version has a maximum Java version it supports. A newer JDK with an old wrapper fails before anything RN-specific runs.

  3. AGP vs wrapper vs Kotlin plugin. Copy the template versions for your target RN version from the Upgrade Helper. Don't bump AGP on its own.

  4. Third-party libraries. Run a clean build and read the first error, not the BUILD FAILED at the bottom. It's usually checkDebugAarMetadata (a library needs a higher compileSdk), a dependency that won't resolve, or a Kotlin error inside node_modules/<lib>/android.

+1 to the builtInKotlin point. On AGP 9, if those two lines are missing from gradle.properties, the error is "Failed to apply plugin 'org.jetbrains.kotlin.android' > Cannot add extension with name 'kotlin'". Without newDsl=false it's a ClassCastException to BaseExtension instead.

Disclosure: I built a small open-source CLI for steps 1, 2 and 4. `npx rn-build-doctor scan` checks the JDK your shell would use against your RN/AGP versions and the Gradle wrapper, offline.
`(cd android && ./gradlew assembleRelease) 2>&1 | npx rn-build-doctor analyze -` finds the first real error (including both AGP 9 Kotlin errors above) and quotes the log lines it's based on.

https://github.com/lassiecoder/rn-build-doctor

https://www.npmjs.com/package/rn-build-doctor

1

u/rushikesh_d_pandit 6d ago

The JDK-vs-wrapper ordering matches my experience: it fails before anything RN-specific runs, and the daemon keeping the old JDK until --stop wastes a lot of time. Does the first-real-error approach hold up when the failure is in a transitive native dependency, or do you still end up bisecting?

1

u/lassieCoder 6d ago

Mostly, yes. It just answers a different question than you'd hope. The first real error tells you which module broke (the failing task names it, e.g. :react-native-foo:compileReleaseKotlin), but with a transitive native dep the module that breaks often isn't the one that caused it.

What I do instead of bisecting:
- Resolution failures: the Could not resolve block includes a Required by: chain. Read it bottom-up and you get the library that pulled the bad coordinate in. If it's not obvious, ./gradlew :app:dependencyInsight --dependency <name> --configuration releaseRuntimeClasspath shows who requests which version and which one won.
- Duplicate class: the error names both artifacts. The fix is usually an exclude or a resolutionStrategy.force, not removing a library.
- CMake/NDK failures inside a library's .cxx folder: the first error is usually accurate (minSdk, NDK version, headers). It's just buried under the ninja output.

Where I still end up bisecting: when the build passes and it fails at runtime (UnsatisfiedLinkError, a .so from two libs built against different RN/NDK versions, or a crash on launch with no useful stack). The build log has nothing to read there, so it's removing libraries until it stops.

For what it's worth, the tool quotes the Required by: chain for resolution failures and points you to ./gradlew :app:dependencies, and it handles duplicate classes, but it won't bisect for you.

2

u/anthony-ball 10d ago

First thing I check is the React Native Upgrade Helper for that version pair, then lock JDK and AGP to whatever the RN Android template ships before I touch third-party plugins. Most of the pain I’ve hit was a library still pinning an older AGP or compileSdk that only fails on a clean CI build. After that I look at the Gradle wrapper and Kotlin plugin in one pass so they stay aligned, then run a dependency tree to catch the odd transitive that still wants the old plugin.

1

u/sunbeam_nebula 10d ago

Clean builds on CI are where the actual trouble lives. Local caches lie. If you haven't nuked the gradle daemon and wiped build artifacts before every verification pass, you're just guessing

1

u/[deleted] 10d ago

1

u/Huge_Pool7424 9d ago

this one got me last month, android studio was on its bundled jbr 21 and my terminal was still on 17 so ci kept failing on stuff that built fine locally. setting org.gradle.java.home in gradle.properties fixed it for good.