A transitive native dependency crashed silently during Android plugin registration, taking down Firebase, path_provider, and every other platform channel with it. The error message pointed everywhere except the actual cause. Here's how I traced it.
My Flutter voice-notes app, Loose Tongues, worked perfectly in local debug and release builds. But every deployed APK from CI failed with:
PlatformException(channel-error, Unable to establish connection on channel:
"dev.flutter.pigeon.firebase_core_platform_interface.FirebaseCoreHostApi.initializeCore")
Firebase wouldn't initialize. The app showed a blank screen. No auth, no Firestore, nothing.
The error message says "Firebase init failed," so naturally I spent hours investigating Firebase:
FirebaseApp initialization successful, but the Dart-side error persisted.Then I noticed something: path_provider was also failing with the exact same "Unable to establish connection on channel" error. And google_fonts. Every Pigeon-based plugin was dead.
I created a fresh Flutter 3.41.5 project, added firebase_core with the same google-services.json and package name, built a release APK, and installed it on the same emulator.
It worked.
Same Flutter version, same Firebase config, same emulator, same signing key. The only difference was the other dependencies. The bug was in the app's dependency tree, not Flutter or Firebase.
I'd been filtering adb logcat for Flutter-level messages. The real error was in the Android native logs, which I'd been ignoring:
E/GeneratedPluginsRegister: Tried to automatically register plugins with
FlutterEngine but could not find or invoke the GeneratedPluginRegistrant.
E/GeneratedPluginsRegister: java.lang.reflect.InvocationTargetException
...
Caused by: java.lang.Error: FFmpegKit failed to start
Caused by: java.lang.UnsatisfiedLinkError: Bad JNI version returned
from JNI_OnLoad in "libffmpegkit_abidetect.so": 0
FFmpegKit's native library was crashing during JNI_OnLoad. Flutter's GeneratedPluginRegistrant registers all plugins in a single method call — alphabetically by package name. One unrecoverable crash aborts the entire registration. Every plugin registered after ffmpeg_kit never initialises. That includes firebase_core, path_provider, and everything else.
I wasn't using FFmpegKit directly. The dependency chain was:
loose_tongues
└── whisper_ggml ^1.7.0 # on-device Whisper transcription
└── ffmpeg_kit_flutter_new_min ^2.1.0 # audio format conversion
└── libffmpegkit_abidetect.so # 💥 crashes on load
whisper_ggml uses FFmpegKit to convert audio formats (e.g., m4a to WAV) before passing them to Whisper. The native FFmpegKit library worked on older Flutter/Kotlin/Gradle combinations but broke with Flutter 3.41's Kotlin 2.2.20 and newer Android build tooling.
Local development was pinned to Flutter 3.38.2, which shipped with an older Kotlin version where FFmpegKit's JNI interface was compatible. CI used channel: 'stable', which silently pulled Flutter 3.41.5 with Kotlin 2.2.20. The newer toolchain changed native linking behaviour, causing JNI_OnLoad to return an incompatible version.
The moment I upgraded local Flutter to 3.41.5, the bug reproduced instantly — on the emulator, in both debug and release mode. The "CI vs local" framing was a red herring. It was always a Flutter version issue; I just hadn't been running the same version locally.
The whisper_ggml package has a fork called whisper_ggml_plus that decoupled FFmpegKit in v1.3.0. The swap was straightforward:
# pubspec.yaml
dependencies:
- whisper_ggml: ^1.7.0
+ whisper_ggml_plus: ^1.3.0
Three additional changes were needed:
AudioEncoder.aacLc to AudioEncoder.wav. Whisper expects 16kHz mono PCM anyway.FFmpegKit.execute('-af volumedetect'). I replaced it with a file-size heuristic — crude but zero dependencies.whisper_ggml_plus adds WhisperModel.largeV3Turbo which needed to be added to switch expressions.APK size dropped from 123MB to 66MB. Firebase initialized on the first launch.
Flutter's GeneratedPluginRegistrant.registerWith() is a single method that registers every plugin sequentially. If any plugin throws during registration, none of the subsequent plugins get registered. There's no isolation, no try/catch per plugin (despite what the generated code looks like — the catch blocks only handle class-not-found, not native load failures).
The Flutter-level error (PlatformException: channel-error) is a downstream symptom. The root cause only appears in Android's native logcat. When debugging platform channel failures, always check:
adb logcat | grep "GeneratedPlugin\|UnsatisfiedLink\|JNI\|Caused by"
My pubspec.yaml didn't mention FFmpegKit. It came in through whisper_ggml, which used it for audio conversion. The 60MB of native .so files were silently bundled into every APK. Audit your transitive dependencies, especially those with native code.
The bug hid for months because local and CI used different Flutter versions. Pinning your CI Flutter version to match local (or vice versa) would have surfaced this immediately. Better yet: test the actual APK that CI produces, not a local build with a different toolchain.
The original code silently caught Firebase init failures and continued, leading to a confusing "No Firebase App [DEFAULT] has been created" error deep in the auth flow. Adding an error screen that displayed the actual PlatformException message cut the debugging loop from "deploy and check" to "read the screen."
The whole investigation took most of a day. The fix was a one-line dependency swap. That's usually how it goes with native interop bugs — the hard part is never the fix, it's finding which layer lied to you.