Skip to content
Blog

jni_util: The Tiny Dart Package Keeping JNI Interop Working

jni_util 1.0.0 is a small dart.dev package for JDK detection that both package:jni and package:jnigen depend on. Here's what it exposes and when you'd call it.

Published on • October 19, 2026

AI Assistant

If you’ve ever generated Java bindings for a Flutter app with jnigen, you’ve already depended on jni_util without knowing it. The package hit 1.0.0 on pub.dev in July 2026 under the verified dart.dev publisher, and it’s one of those infrastructure packages worth understanding: it’s tiny, it’s published by the Dart team, and it solves one specific problem that both halves of Dart’s JNI stack share — finding a working JDK.

What the package actually is

jni_util describes itself in one line: “Shared utility functions for package:jni and package:jnigen.” The changelog for 1.0.0 is equally terse: “Initial version containing shared JDK detection utilities.” It ships with a single runtime dependency (path), BSD-3-Clause licensing, and lives in the dart-lang/native monorepo alongside the rest of the native interop tooling.

The entire public API is four members:

  • javaHome — a Uri? with the Java home path Flutter itself uses, detected via flutter config --machine.
  • javaEnvironment — a copy of Platform.environment with JAVA_HOME pointed at Flutter’s JDK (when detected) and JAVA_HOME/bin prepended to PATH.
  • resolveJavaExecutable(String exec) — resolves names like java, javac, or javap to full paths inside JAVA_HOME/bin.
  • minJavaVersion and maxJavaVersion — constants bounding the supported JDK range.
import 'package:jni_util/jni_util.dart';

void main() {
  // Flutter's bundled JDK, if one is configured.
  print('Java home: ${javaHome ?? "not found, falling back to JAVA_HOME/PATH"}');

  // Resolve tool paths the same way jnigen does.
  final javac = resolveJavaExecutable('javac');
  final javap = resolveJavaExecutable('javap');

  // An environment map you can hand to a spawned process.
  javaEnvironment.forEach((key, value) {
    if (key == 'JAVA_HOME' || key == 'PATH') print('$key=$value');
  });
}

There’s no widget, no plugin, and no Android/iOS runtime code. It’s pure Dart that answers “where is a good JDK on this machine?” — which is exactly what code-generation tooling needs before it does anything else.

Why both jni and jnigen depend on it

The problem jni_util solves shows up clearly in jnigen’s requirements: the generator needs a JDK (officially Java 17 through 21), and it wants the same JDK Flutter is using. Its documented resolution order is: use the JDK configured in Flutter (typically Android Studio’s bundled JDK), and if that fails, fall back to JAVA_HOME and PATH. That detection logic is precisely what javaHome, javaEnvironment, and resolveJavaExecutable implement — so rather than each package carrying its own copy, both package:jni (the runtime support library) and package:jnigen (the bindings generator) take a dependency on jni_util.

That shared-dependency structure is why a “1 like” package can still show millions of downloads on pub.dev: download counts aggregate across every pub get that pulls it in transitively, and nearly every app using JNI bindings pulls it in through both parents.

Where jni_util sits in the interop stack

If you’re new to Dart’s JNI story, the stack has three layers:

  1. package:jni is the runtime. It exposes JNIEnv and JavaVM access (with a GlobalJniEnv abstraction so the env can be used from multiple threads), Jni.spawn for starting a JVM on desktop platforms, the JObject base class that generated bindings extend, and hand-written wrappers for common Java types like JList, JMap, and JInteger.
  2. package:jnigen is the generator. It scans compiled JARs or Java sources, builds an API summary via the JDK’s Doclet API, and emits Dart bindings that call through dart:ffi to JNI. Configuration is a Dart script (tool/jnigen.dart) instantiating JniGenerator — the YAML config format is deprecated.
  3. package:jni_util is the shared plumbing underneath both: JDK discovery and executable resolution.
// tool/jnigen.dart — jnigen needs a JDK; jni_util is how it finds one.
import 'dart:io';
import 'package:jnigen/jnigen.dart';

Future<void> main() async {
  final packageRoot = Platform.script.resolve('../');
  final generator = JniGenerator(
    input: Input(
      classes: ['com.example.in_app_java'],
      sourcePath: [packageRoot.resolve('android/app/src/main/java')],
    ),
    output: Output(
      dart: DartOutput(
        path: packageRoot.resolve('lib/android_utils.g.dart'),
        structure: OutputStructure.singleFile,
      ),
    ),
  );
  await generator.generate();
}

Platform reality checks

The interop stack works best where a JVM is already running. On Android, your Flutter app runs embedded in the Android JVM, so bindings just work. On desktop, you spawn one yourself with Jni.spawn, and for Dart standalone targets you first build the native JNI libraries with dart run jni:setup (they land in build/jni_libs). Windows needs jvm.dll from your JDK on PATH; macOS supports Dart standalone but Flutter-on-macOS support was still marked “not yet” on jnigen’s platform table at the time of writing.

Two operational details bite in production. First, ProGuard prunes classes it deems inaccessible, but JNI class lookup happens at runtime — so release builds throw ClassNotFoundError unless you keep classes with @Keep or proguard rules. Second, reference management: every Java object returned into Dart creates a JNI global reference. NativeFinalizer usually suffices, but if you create references in a loop, use the FFI Arena or release explicitly.

When you’d use jni_util directly

You probably won’t add it as an app dependency — jni and jnigen already bring it in. Where it earns its keep directly is in custom tooling: build scripts that need to invoke javac or javap, CI steps that verify a JDK is present before running binding generation, or diagnostics that print which JDK your Flutter install is actually using. Instead of shelling out to java -version and parsing strings, call resolveJavaExecutable and javaEnvironment — the same resolution the official tooling trusts.

Sources