Skip to main content

JNIgen

Learn how to use JNIgen to generate type-safe Dart bindings for Java and Kotlin APIs.

Overview

#

JNIgen (Java Native Interface generator) is a code generator tool that creates type-safe Dart bindings for Java and Kotlin libraries and APIs. This allows calling Android APIs directly and synchronously from your Dart code.

When to use JNIgen

#

Use JNIgen when you need to call Java or Kotlin libraries, wrap compiled JAR or AAR files, or interact directly with Android from your Dart code.

JNIgen uses direct JNI bindings through dart:ffi, making it the recommended tool when you require high-performance, low-latency interop, or need to pass massive amounts of raw data between Dart and the JVM.

For standard, cross-platform plugin development where you don't have critical data transfers, use Pigeon instead.

Developer flow

#

Generate bindings with JNIgen using the following general flow:

  1. Reference the native classes or JARs.
  2. Write the JNIgen configuration script.
  3. Generate the bindings by running JNIgen.
  4. Call the generated code from Dart.

To start, identify the native Java classes, packages, or compiled JAR/AAR files you want to wrap. Then write a configuration script (typically tool/jnigen.dart). This configuration specifies where the native files are located, what packages to include, and where to output the generated Dart code.

Next, run the configuration script (dart run tool/jnigen.dart), which parses the classes and generates the corresponding Dart bindings.

Last, import the generated Dart file into your app and call the functions directly. JNIgen communicates directly through the underlying FFI layer, so refer to the dart:ffi guide to learn how to manage native memory lifetimes and execute native calls without blocking the UI thread.

Examples

#

To explore complete implementations of this flow, including how to configure the generator, refer to the permissions_plugin demo and the android_toast_demo in the official Flutter native interop demos repository.

For a production package on pub.dev that uses JNIgen to wrap the Android Cronet HTTP library, check out package:cronet_http (source on GitHub).

Constraints and limitations

#

Keep these general considerations and limitations in mind when working with JNIgen:

Threading considerations

#

JNIgen calls run synchronously on the calling thread. Watch out for two common scenarios:

  • When calling platform APIs that must run on the platform thread, use runOnPlatformThread from dart:ui.
  • Dart callbacks from Java run on the isolate that created them. This can lead to deadlocks if a blocked Java thread waits for a blocked Dart thread.

To learn more, consult the JNIgen threading guide.

Lifecycle of Java objects

#

JNI wrappers use Java global references. There is a hard limit of 50,000 active references. Call .release() or use .using() blocks to prevent memory leaks. Use WeakReference to break reference cycles between Dart and Java GCs when implementing interfaces, as shown in the JNI object lifecycle guide.

JVM runtime type checks

#

JNI wrappers are Dart extension types that compile down to JObject at runtime, so Dart's native is and as keywords don't check the Java runtime type. Use x.isA(Foo.type) and x.as(Foo.type) instead, as shown in the Java runtime types guide.

General debugging

#

Tracking down JNI memory allocation bugs like DoubleReleaseError and UseAfterReleaseError can be difficult. Set Jni.captureStackTraceOnRelease = true to log stack traces and help trace leaks, as explained in the JNIgen debugging guide.

Resources

#

To learn about generating Java and Kotlin bindings with JNIgen, refer to the following resources: