Published November 13, 2026
FFIgen and JNIgen Reach Stable: What Direct Native Interop Changes for Plugin Authors
A day after Flutter 3.50 shipped, the Flutter team published the announcement that plugin authors have been waiting on for a year: FFIgen for Objective-C and JNIgen for Java and Kotlin are stable, and Pigeon can use both under the hood. The post is short and unusually concrete, which suits a change that removes an entire category of boilerplate from plugin development.
Why Platform Channels Were the Problem
Platform channels work by message passing. Dart serializes arguments into a binary buffer, sends them across a bridge, the native side deserializes and runs the API, then the result travels back through the same path in reverse. It is asynchronous by design, which keeps the UI responsive but makes instant native calls impossible without async plumbing around them.
The maintenance cost is the part that accumulates. Message handlers are addressed by string names, so a renamed native method becomes a runtime failure rather than a compile error, and a native API change has to be mirrored by hand on both sides. Pigeon solved the typing half of this: describe the API once, generate matching code for both platforms, and method-name mistakes fail at build time. Under the hood, though, the generated code still used platform channels, so every call still paid the serialization round trip.
What the Stable Generators Do
FFIgen now covers C and Objective-C, and JNIgen covers Java and Kotlin, from Android system APIs to a project's own JARs and AARs. Both emit plain Dart bindings, so they work in any Dart project with or without Flutter. Because the call path is native rather than message based, synchronous calls become possible, and large buffers or native object handles move between runtimes without being copied into a serialized form. In many cases an app can call a platform API without writing native code at all.
The trade-off is explicit in the announcement. The generated bindings are low level, and memory management and thread handling become the developer's responsibility. That is fine for a single-platform wrapper around sqlite3 or an Android-only API, and less fine for a plugin that has to behave identically on four platforms.
Pigeon with native interop is the answer to that second case. Pigeon 30 and later generate FFI and JNI bindings from the same contract file, handling data conversion and native object lifetimes, and only generating what the plugin needs. It is available today as a preview on Android, iOS, and macOS. Windows and Linux continue to use platform channels, and a single Pigeon file can produce both kinds of output.
Enabling it is additive. In the ConfigurePigeon annotation that already sits at the top of the Pigeon definition files, add appDirectory pointing at the compiled app, usually the plugin's example directory, then set swiftOptions: SwiftOptions(useFfi: true) and kotlinOptions: KotlinOptions(useJni: true). The Swift or Kotlin implementation stays where it is, and it is still the place where platform APIs get called.
- shared_preferences is already migrated and serves as the reference implementation
- The migration guide follows that port step by step, including build and registration changes
- A native interop migration skill exists for teams driving the work through an AI coding agent
For teams weighing raw generators against Pigeon, the announcement makes a fair case for Pigeon even as agents get better at writing idiomatic wrappers. What Pigeon adds is a consistent cross-platform API, memory safety across two runtimes, unit-testable plugin code, orchestration of each platform's toolchain, and generated output that stays deterministic as the contract evolves.
The practical sequence for an existing plugin is short. Add the interop dependencies, flip the two options, re-run Pigeon, then fix whatever the build complains about in registration. Doing this on a branch with the example app as the first test target is cheaper than discovering a lifetime bug in production, since double frees and use-after-free mistakes in native bindings will not appear as Dart exceptions.
One detail worth noting for anyone reading the Flutter 3.50 release post first: direct native interoperability is listed there as a headline feature, but the mechanics, the preview status of Pigeon's mode, and the platform limits live in this companion post. The two announcements describe the same work from different angles, and the plugin-focused one is the version with the migration instructions.