Gradle Build Failed: Fix the Minimum Version
Match the wrapper to your AGP: Gradle build failed means the wrapper is older than the plugin's minimum.
20+ years shipping production backend systems. Notes here come from systems that actually shipped.
- ✓A Flutter project with the android directory
- ✓Terminal access to edit wrapper files
- ✓JDK 17 available for AGP 8 builds
- Gradle build failed with a minimum-version message means your Android Gradle Plugin needs a newer Gradle than gradle-wrapper.properties currently downloads
- AGP 8.x requires Gradle 8.x and JDK 17, so check the plugin version in settings or app-level build files before touching anything
- Bump distributionUrl in android/gradle/wrapper/gradle-wrapper.properties to the required Gradle release, then run with --refresh-dependencies
- Keep flutter upgrade, AGP, Gradle, and JDK in lockstep, and verify with flutter doctor and a clean assembleDebug build
Think of the Android build as a power tool with swappable batteries. The Android Gradle Plugin is the drill, and Gradle itself is the battery pack — each new drill generation needs a newer battery voltage. When Flutter upgrades the drill but your project still stocks the old battery, the build refuses to start and prints a minimum-version error. and changing it gets the drill spinning again.
You run flutter build apk, Gradle spins for a minute downloading dependencies, then dies with Minimum supported Gradle version is 8.7. Current version is 8.3. Nothing in your Dart code changed — the failure lives entirely in the Android build layer. This error spikes after every flutter upgrade, because the new Flutter pins a newer Android Gradle Plugin, and that plugin demands a newer Gradle than your project's wrapper still downloads.
The version triangle confuses everyone the first time: Flutter, the Android Gradle Plugin (AGP), Gradle itself, and the JDK must all agree. AGP 8.x needs Gradle 8.x and JDK 17; hand it Gradle 7 or JDK 11 and the build fails with messages that blame each other. Developers often fix the wrong corner — upgrading Gradle when Java is the problem — and burn hours.
This guide untangles the triangle. You will read the error to identify which corner is stale, look up the exact AGP-to-Gradle minimum, bump the wrapper's distributionUrl correctly, align the JDK, and verify with a clean build. By the end, minimum-version failures become a ten-minute properties edit instead of a lost afternoon.
Reading the Error: Which Corner Is Stale
Minimum-version messages are unusually honest. Minimum supported Gradle version is 8.7. Current version is 8.3 names both sides: the plugin needs 8.7, the wrapper supplied 8.3. Your job is bumping supply to demand — edit the wrapper, not the plugin. Downgrading the plugin to match old Gradle trades a one-line fix for stale build tooling and future incompatibilities.
JDK errors disguise themselves nearby. Unsupported class file major version 65 or A problem occurred evaluating settings means Gradle itself runs on the wrong Java — AGP 8 needs JDK 17, and major version 65 is Java 21 bytecode that old toolchains cannot read. If your error mentions class files, Java, or toolchain instead of two Gradle numbers, skip the wrapper and fix JAVA_HOME first.
Flutter doctor settles arguments fast. Run flutter doctor -v and read the Java and Android toolchain sections before editing anything: a healthy triangle shows Flutter current, AGP pinned, wrapper matching, JDK 17. Change exactly one corner per attempt and rebuild — stacked simultaneous edits leave you unsure which one worked, and the next failure teaches you nothing. Screenshot the triangle readout into the incident channel so the whole team reasons from the same numbers.
The AGP-to-Gradle Map You Must Respect
Every AGP release publishes a minimum Gradle version, and the plugin enforces it at configuration time — before compiling anything. The landmarks to memorize: AGP 8.0 needs Gradle 8.0 as its floor, AGP 8.5 raises the floor to Gradle 8.7, and AGP 9.x moves the whole line to Gradle 9.x. Minor AGP bumps inside a series can raise the minimum too, which is why a routine flutter upgrade breaks a wrapper that worked last month.
The same table names the JDK floor: AGP 8 requires JDK 17 to run Gradle, full stop. Developers who bump Gradle while running JDK 11 graduate from one error to another and conclude the bump did not work — it did, and now Java is the stale corner. Check Google's AGP release-notes compatibility table for your exact plugin version rather than trusting memory, since floors move.
Write the mapping into your upgrade runbook, not your head. Before any flutter upgrade, record current AGP, wrapper Gradle, and java -version; after upgrading, diff the AGP pin and look up its new minimum. If the minimum exceeds your wrapper, bump the wrapper in the same pull request. Ten minutes of table-checking prevents the six-hour CI outage in our incident story. Pin the table link at the top of the runbook so nobody hunts for it mid-upgrade.
Bumping distributionUrl Without Breaking the Wrapper
The wrapper properties file is small and unforgiving. distributionUrl names the exact Gradle zip to download — change only the version segment, preserving the services.gradle.org host, the distributions path, and your project's -all- versus -bin- flavor. Switching flavors accidentally changes what ships in the distribution and confuses every machine that already cached the other one.
Edit with intent: gradle-8.3-all.zip becomes gradle-8.7-all.zip, nothing else moves. Commit the file — wrapper bumps that live only on one laptop cause works-on-my-machine mysteries when CI downloads the old release. Then run flutter clean before rebuilding: stale build outputs compiled against the old Gradle produce phantom errors that look like the bump failed when it actually succeeded.
Rebuild with fresh resolution once. flutter build apk --debug --refresh-dependencies forces dependency metadata to re-resolve against the new engine; subsequent builds can drop the flag and run at normal speed. Watch the log's Downloaded Gradle line to confirm the runner fetched the version you named — trust the download line, not your memory of the edit. If the line still shows the old release, the edit is uncommitted or the runner cache needs one targeted wipe.
Aligning the JDK: AGP 8 Means Java 17
Gradle runs on the JVM, so the JDK is a full corner of the triangle. AGP 8 requires JDK 17 to run: launch Gradle with JDK 11 and configuration fails before version checks even matter, with errors about class file versions or unsupported runtimes. Installing JDK 17 is not enough — JAVA_HOME must point at it in every shell and CI runner that builds, and Android Studio's Gradle JDK setting must agree.
Diagnose in one pass. java -version shows the runtime on PATH, echo of JAVA_HOME shows what Gradle daemons inherit, and Android Studio's Settings Build Tools Gradle panel shows what the IDE uses — all three must say 17 for AGP 8 builds. Mismatches between terminal and IDE are the classic split: command-line builds pass while IDE syncs fail, or the reverse, and each side blames the other.
On CI, pin the JDK in the image or setup step rather than hoping the default is right. A setup-java step naming 17, or a base image with JAVA_HOME preset, removes the entire category. Log java -version at the top of every Android job so the next failure opens with the answer instead of a guessing game. When IDE and terminal disagree, believe the one that matches CI and fix the other. Document the blessed JDK path per OS so new hires align on day one.
Keeping Flutter, AGP, and Gradle in Lockstep
Flutter upgrades move the AGP pin, and the AGP pin moves the Gradle floor — so treat flutter upgrade as an Android build change, not just Dart. Before upgrading, record the current triangle: flutter --version, the com.android.application pin, the wrapper version, and java -version. After upgrading, diff the AGP pin first; if it moved, look up its new minimum and bump the wrapper in the same branch.
Resist partial upgrades. A new Flutter with a hand-downgraded AGP buys short-term green builds and long-term drift from the tested template — future upgrades then break harder. Stay on the template's AGP, meet its Gradle and JDK floors, and file issues upstream if the combination genuinely fails. The template is tested as a unit; your custom mix is not.
When everything still fails, bisect cleanly. flutter clean removes stale outputs, --refresh-dependencies re-resolves metadata once, and a fresh checkout rules out local uncommitted edits. Change one corner per attempt and keep notes — the engineer who writes down each attempt solves it in four steps, while the one who flails solves it in forty. A written bisect log also becomes next quarter's runbook entry. Future upgrades then start from evidence instead of folklore.
Verifying With a Clean assembleDebug
Trust only a clean build from a committed tree. Commit the wrapper bump, run flutter clean to drop outputs compiled under the old engine, and build with flutter build apk --debug. A green assembleDebug plus the Downloaded Gradle line showing your target version is the proof — anything less, like an incremental hot-reload session, can pass while CI still fails.
Verify the artifact, not just the exit code. Install the debug APK on a device with flutter install and launch it past the first frame; resource-merging and dexing failures sometimes surface at install or launch rather than compile time. Then run the same command on CI and compare version lines — local and remote must agree on Gradle, AGP, and Java before you declare victory.
Lock the win. A CI step echoing the triangle versions each build turns future mismatches into readable diffs, and requiring review on wrapper and AGP lines stops silent drift. Minimum-version errors should cost your team ten minutes exactly once — the second occurrence means the guardrails, not the properties file, need fixing. Treat a repeat as a process bug and fix the checklist, not just the version. Green builds you cannot explain are just red builds waiting for Friday.
Flutter Upgrade Broke Android CI for 6 Hours on Gradle 8.3
- Read minimum-version errors literally: they name the required and current versions, so the fix is almost always bumping distributionUrl, not rebuilding infrastructure or wiping caches.
- Review wrapper properties in every flutter upgrade diff. The upgrade touches many files, but AGP and wrapper lines are the ones that break Android builds — gate them with a second reviewer.
- Log the full version triangle on every CI build. Printing Flutter, AGP, Gradle, and Java versions turns the next failure into a one-line diff instead of a six-hour hunt.
| File | Command / Code | Purpose |
|---|---|---|
| io | Future<void> main() async { | Reading the Error |
| io | const Map<String, String> floors = <String, String>{ | The AGP-to-Gradle Map You Must Respect |
| io | Future<void> main(List<String> args) async { | Bumping distributionUrl Without Breaking the Wrapper |
| io | Future<void> main() async { | Aligning the JDK |
| io | Future<void> main() async { | Keeping Flutter, AGP, and Gradle in Lockstep |
Key takeaways
Common mistakes to avoid
5 patternsDowngrading AGP to match the old wrapper
Wiping all Gradle caches as the first response
Editing the version plus flavor or host
Forgetting to commit the wrapper bump
Upgrading Gradle while running JDK 11
Interview Questions on This Topic
What does Minimum supported Gradle version is 8.7, current is 8.3 mean?
Frequently Asked Questions
20+ years shipping production backend systems. Notes here come from systems that actually shipped.
That's Flutter. Mark it forged?
5 min read · try the examples if you haven't