Haunted Flutter

Flutter and the case of the haunted JDK

Flutter and the case of the haunted JDK | Blog

Flutter and the case of the haunted JDK

I built my self a Pomodoro app a couple of years ago, and, I was kind of happy with it at the time, I never bothered with putting it in the Play Store because I built it for me, I just wasn’t happy with the advertisements that came with the free ones that existed and was less happy to pay for something so simple. So I built my own, of course, in terms of my time, it was far more expensive than just buying an app, but I enjoy fighting with Flutter, so I thought it was a nice little thing for me to get into.

Now, fighting with Flutter seems very apt for the rest of this blog post, which, is mostly being posted as a reminder to future on how to fix this, the short version is that I hard-coded a config line that persisted through various flutter installs and I completely forgot about it, so spent about 2 hours this weekend fighting with Flutter.

TL;DR (The Fix)

If flutter doctor complains about a Java path that no longer exists, and nothing you do seems to change it:

flutter doctor -v

If you see a line like:

“This JDK is specified in your Flutter configuration.”

Then Flutter itself has a hard-coded Java path saved globally.

Fix it with:

flutter config --jdk-dir="$(/usr/libexec/java_home -v 17)"

Or clear it entirely:

flutter config --clear-jdk-dir

That’s it. No Gradle voodoo required.


Flutter, Java, Gradle, and the Case of the Haunted JDK

The Symptom

After returning to an old Flutter project (last built a couple of years ago), I ran:

flutter doctor

and was gifted with this error:

Android toolchain - develop for Android devices
✗ Cannot execute /opt/homebrew/Cellar/openjdk@17/17.0.13/.../bin/java

Okay, I haven’t been here for ages, I don’t work with Flutter on a daily basis. I started by checking Java, it was there and I had an environment variable in my zprofile. It looked like I also had a corrupted Flutter install, so that was an easy fix, I just removed the clone from my ~/Development space and re-cloned from the github source. This did not fix the issue, now I had assumed that the configuration would be inside that corrupted flutter installation location, but re-installing fixed flutter but not this Gradle issue.

Maybe it was a caching thing, I cleared the cache:


rm -rf ~/.gradle/caches
rm -rf ~/.gradle/daemon
rm -rf android/.gradle
rm -rf android/build

This was not the answer I was looking for. Maybe an Android Studio thing, it turns out I had an aging version of this application, so I downloaded and replaced the old installation with the latest stable release. Still no ice cream for me:

  • Java was installed
  • Java worked in the terminal
  • Android Studio was freshly installed
  • Flutter had been re-cloned
  • Gradle caches were cleared

And yet Flutter stubbornly insisted on using Java 17.0.13, a version that no longer existed.


Why This was so Confusing

Because everything else was correct:

  • java -version → ✅ Java 17.0.18
  • JAVA_HOME → ✅ Correct
  • Android Studio → ✅ New version
  • Homebrew → ✅ Up to date
  • Gradle config files → ❌ Nothing suspicious

This mades it feel like:

  • Gradle was broken
  • Android Studio was broken
  • Flutter was broken
  • Or macOS was haunted

In reality…


The Real Cause (The “Aha” Moment)

I went to a friendly LLM and described the steps taken so far, the first answers were a bit vanilla, restart this, restart that, but after a bit of back-and-forth we arrived at the crux of the matter; Flutter stores global user configuration outside the SDK directory. I want to ask Why, but it probably makes perfect sense for people working on FLutter daily and not wanting builds to be disrupted by differing versions of Java. For me, it was not only an “Aha” moment, it was an “Oh no, I vaguely remember doing this before” moment, suddenly I remembered that another app I built with Flutter about 12 months ago also stumbled into an issue and I hard-coded something to get around the issue. I checked my terminal history:

flutter config --jdk-dir=/opt/homebrew/Cellar/openjdk@17/17.0.13/libexec/openjdk.jdk/Contents/Home

That command:

  • Overrides all Java auto-detection
  • Persists across Flutter reinstalls
  • Survives SDK upgrades
  • Ignores JAVA_HOME
  • Ignores Android Studio

And crucially:

It stores the absolute Homebrew Cellar path

So when Homebrew later upgraded Java:

17.0.13 → 17.0.18

…the old directory was deleted, but Flutter kept pointing at it.


Where This Configuration Lives

Flutter’s global config lives in:

~/.flutter_settings

This file is not removed when you:

  • Delete the Flutter SDK
  • Clone Flutter again
  • Reinstall Android Studio
  • Clear Gradle caches

This design is intentional — but very surprising, to me at least.

You can inspect it with:

cat ~/.flutter_settings

Why Nothing Else Fixed It

Flutter resolves Java in this priority order:

  1. Explicit Flutter config (flutter config --jdk-dir) ← highest priority
  2. Gradle overrides
  3. Android Studio settings
  4. JAVA_HOME
  5. Auto-detection

Because level 1 was set, everything else was ignored.

That’s why:

  • Rebooting didn’t help
  • Reinstalling tools didn’t help
  • Killing Gradle didn’t help
  • Updating shell config didn’t help

Flutter was doing exactly what it was told — just a long time ago, in a galaxy far, far, away.


The Final Fix (Again, Clearly)

Option A: Pin Java (this is what I chose, but actually, option B is probably better for me)

flutter config --jdk-dir="$(/usr/libexec/java_home -v 17)"

Stable, predictable, CI-friendly. I think this makes sense if I was building in flutter regularly, but for me, I will likely fight with Flutter for a day or two, get some of my app ideas built or updated and then forget all about this nightmare until I decide to to try something new in a few months time.

Option B: Return to auto-detection

flutter config --clear-jdk-dir

More flexible, but more fragile, I think. For me, maybe it would be a good option as who knows how many times Java will be updated before I come back to Flutter building mode, but for a professional dev, or someone working with this stuff daily, it probably introduces lots of fragility as you’re risking your dev-path each time there is a new Java.


How I Found the Smoking Gun

After fixing the issue, running:

history | grep flutter

Revealed this from hundreds of commands ago:

flutter config --jdk-dir=/opt/homebrew/Cellar/openjdk@17/17.0.13/libexec/openjdk.jdk/Contents/Home

That was my mystery solved, alas, it took me like 2 hours to get there, but hopefully it will be burned in my mind and I’ll be fight-ready the next time this happens to me.


Lessons Learned

  • Flutter has hidden global state
  • Homebrew Cellar paths are not stable
  • Gradle is often blamed, but not always guilty
  • flutter doctor -v is your best diagnostic tool

And most importantly:

If Flutter mentions a Java path that doesn’t exist anymore, check Flutter’s own config first.


Note to Future Me

Before reinstalling half of the toolchain:

flutter doctor -v
flutter config
cat ~/.flutter_settings

This post exists so I never have to debug this particular issue again. I asked the same friendly LLM to generate a blog post image to support this post and I think it hit the nail on the head, bravo:

Is JDK Haunted?