DEV Community

Navya Sinha
Navya Sinha

Posted on

Why Gradle 9 broke our Flutter SDK, and how we're fixing it

One of our partners upgraded their Android build tools and their app stopped building. The cause wasn't their code. It was a single outdated line in our Flutter plugins, pointing at a download server the new tools no longer know about.

The fix is two lines per plugin. The more interesting story is why it happened, why the fix lives where it does, and why we're deliberately not shipping the bigger upgrade at the same time.

A 60-second primer 🧠

Three names that come up regulary:

  • Gradle is the build tool. It takes an Android project, downloads the libraries it needs and compiles it into an app. Think of it as the cook.
  • AGP (Android Gradle Plugin) is Google's add-on that teaches Gradle how to build Android apps. It's the recipe book, and it has versions: 8.x, and now 9.x.
  • Kotlin is the language our Android plugin code is written in. Until AGP 9 you needed a separate Kotlin add-on (KGP), like a second cookbook. AGP 9 binds Kotlin into the main recipe book, which Google calls "built-in Kotlin".

The most important point: the partner's app picks the versions of all three, not us. Our SDK has to work with whatever kitchen it's dropped into.

What happened? πŸ”

The partner builds its app with Gradle 9.1 and AGP 9.0.1, and the app uses our plugin. The build failed during setup, before our SDK ran a single line.

The culprit was one word in our plugin's build file: jcenter().

That line tells Gradle to download libraries from JCenter, an old public library server. JCenter stopped taking new uploads in 2021 and since then has only passed on copies of what's on Maven Central, the main public server. Gradle deprecated jcenter() in Gradle 7 and deleted it completely in Gradle 9. So when Gradle 9 reads our file, it hits a command it doesn't recognise and stops.

It wasn't just one partner's problem. Every version of our Gateway and Loans Flutter plugins we've published has that line, so any partner on Gradle 9 would hit the same wall.

Why is our file in their build at all?πŸ€”

When a partner adds a Flutter plugin, Flutter pulls the plugin's Android folder into the partner's build, and the partner's Gradle runs it.

Our native Android SDK works differently: partners download it as a finished package from our Artifactory, and its build files never run on their machines. That's why the fix belongs in the Flutter plugin repo and not the Android SDK repo.

Why we don't just use the newest everything πŸ€·β€β™€οΈ

It's tempting to ask why our SDK wasn't already on the latest tools. The answer is that an SDK is a guest in someone else's kitchen.

Each partner decides when to upgrade their tools, and upgrading touches their whole app, so some move fast and many move slowly. If our SDK demanded the newest tools, every partner who hadn't upgraded would be stuck on an old version of our SDK, without our bug fixes.

So we deliberately support old setups: our plugins accept Flutter versions as old as 3.0 and Android versions as old as 5.0 (API 21).

The catch is that supporting old setups means keeping some old pieces in our files. Usually that's harmless. But sometimes the newest tools delete an old piece outright, and then partners on the newest tools break. That's exactly what happened with jcenter().

We call this staying on the borderline: our SDK has to work at both ends at the same time, the oldest setup partners still run and the newest one they're moving to.

The fix: small now, bigger later πŸ”¨

We're shipping this as two separate changes, on purpose.

The patch

In both the Gateway and Loans plugins, jcenter() becomes mavenCentral(). That's two lines per plugin. Maven Central holds the same libraries JCenter was passing on, and it works on every Gradle version we support, old and new. Nothing else changes: same SDK features, same native SDK versions, same minimum Flutter and Android versions. A partner on older tools won't notice anything.

The upgrade

This moves the plugins fully onto AGP 9's way of doing things. It drops the separate Kotlin add-on and relies on AGP 9's built-in Kotlin, renames some old settings to their new names, and raises the minimum Flutter version to 3.44. It also upgrades our demo app to the newest tools so we can test it.

Why not just ship the upgrade?

Because it breaks the first rule. Raising the Flutter minimum, and depending on built-in Kotlin, would lock out partners who haven't upgraded. We expect that would include the partner who reported the bug, since they run AGP 9 with built-in Kotlin switched off. The patch unblocks everyone today, and the upgrade can follow as a planned, announced release.

One thing that confuses people: neither bumps the AGP version inside our plugins. That's because the plugins never choose AGP; the partner's app does. Being "AGP 9 ready" means our build file works when AGP 9 runs it.

What it means for partners?

The easiest way to see the impact is to picture three kinds of partner.

Partner A hasn't upgraded yet and is still on Gradle 8 or older

  • Today: everything works.
  • After the patch: still works, and nothing changes for them.
  • After the upgrade: likely breaks. Below Flutter 3.44 they can't install it at all, and without built-in Kotlin our plugin's Kotlin code wouldn't be compiled.

Partner B upgraded to AGP 9 but kept Kotlin the old way by switching built-in Kotlin off.

  • Today: the build fails on jcenter().
  • After the patch: the jcenter() error is gone, and we expect the build to work. We're testing that now.
  • After the upgrade: likely breaks, because the upgrade expects built-in Kotlin and they've switched it off.

Partner C upgraded fully and uses built-in Kotlin. This needs Flutter 3.47 or newer.

  • Today: the build fails on jcenter().
  • After the patch: probably still fails. Our plugin still adds the old Kotlin add-on, and built-in Kotlin replaces it.
  • After the upgrade: works. This is the setupthe upgrade is built for.

So the patch helps A and B and hurts nobody, and the upgrade is what C needs. Neither covers all three yet.

The open question for the upgrade is whether one version of our plugin can serve both B and C, for example by adding the old Kotlin add-on only when built-in Kotlin is switched off.

Top comments (1)

Collapse
 
ahmedznouda profile image
Ahmed ZNOUDA • • Edited

Clear write-up, and splitting the two-line patch from the planned upgrade is the right order for an SDK that lives in other people's builds.

On your open question (one plugin version for both B and C), one approach I'd try. It's untested on my side, our own app is still on AGP 8.11 and Gradle 8.14: decide in the plugin's build.gradle whether to apply the Kotlin plugin, from the AGP version and the built-in Kotlin flag (if I have the property name right).

groovy
def agpMajor = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
def builtInKotlin = agpMajor >= 9 &&
(project.findProperty('android.builtInKotlin') ?: 'true').toString().toBoolean()
if (!builtInKotlin) {
apply plugin: 'kotlin-android'
}
`

Partners A (AGP 8) and B (AGP 9, flag off) get the separate Kotlin plugin, partner C gets built-in Kotlin. The catch: a plugins {} block can't be conditional, so this only works with the older apply plugin style, and any kotlinOptions block needs the same guard.

Whichever way you go, three tiny example apps in CI, one per partner type, would turn the A/B/C table into a test.