Repository navigation
Description
Summary
In a minified Android release build, every call that goes through a Kotlin suspend function via jnigen bindings fails. Flutter turns on R8 for release builds by default.
java.lang.NoSuchMethodError: no non-static method "Lcom/example/test_plugin/NativeInteropHostIntegrationCoreApiRegistrar;.noopAsync(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;"
The bound class is annotated with @Keep, so it and its methods survive R8. But R8 renames kotlin.coroutines.Continuation and rewrites the descriptors of kept methods to match. The descriptor baked into the generated bindings, (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;, no longer exists.
package:jni and jnigen-generated bindings look up six Kotlin stdlib classes by name. jni's consumer rules only keep com.github.dart_lang.jni.**, and kotlin-stdlib ships no R8 rules of its own. As a result, every app that uses suspend functions through jni has to find and add these rules itself.
Kotlin classes looked up by name
| Class | Used for | Source |
|---|---|---|
kotlin.coroutines.Continuation |
Descriptor of every suspend method, in generated calls and in implement dispatch; resumeWith |
Generated code, kotlin.dart#L53-L58 |
kotlin.coroutines.intrinsics.CoroutineSingletons |
Detecting COROUTINE_SUSPENDED |
kotlin.dart#L14-L16 |
kotlin.coroutines.intrinsics.IntrinsicsKt |
getCOROUTINE_SUSPENDED() |
kotlin.dart#L36-L42 |
kotlin.Result |
value field |
kotlin.dart#L28-L34 |
kotlin.Result$Failure |
exception field and constructor |
kotlin.dart#L19-L25, #L63-L65 |
kotlin.Unit |
INSTANCE, returned from Dart implementations of suspend fun …: Unit |
kotlin.dart#L44-L47 |
For example, jnigen 1.0.1 generates:
static final _id_noopAsync = NativeInteropHostIntegrationCoreApi._class.instanceMethodId(
r'noopAsync',
r'(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;',
);
// ...and, in `implement`:
if ($d == r'noopAsync(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;') {Proposed fix
Add the rules to pkgs/jni/android/consumer-rules.pro so that every app depending on jni gets them:
-keep class com.github.dart_lang.jni.** { *; }
+
+# Kotlin classes that package:jni and jnigen-generated bindings look up by name
+# to support suspend functions.
+-keep class kotlin.Unit { *; }
+-keep class kotlin.Result { *; }
+-keep class kotlin.Result$Failure { *; }
+-keep class kotlin.coroutines.Continuation { *; }
+-keep class kotlin.coroutines.intrinsics.CoroutineSingletons { *; }
+-keep class kotlin.coroutines.intrinsics.IntrinsicsKt { *; }These rules only stop R8 from renaming or removing six small stdlib classes.
Optional follow-ups:
- Remove
-keep class kotlin.coroutines.** { *; }from thekotlin_pluginexample's app rules. The example then checks that the consumer rules are enough. - Run a suspend function test in a release build on CI.
Verification
We hit this in Pigeon's JNI support (flutter/flutter#193880). We ran Pigeon's native interop integration suite in a minified release build. The suite has 339 tests, uses jnigen 1.0.1 bindings, and every Pigeon-generated class was annotated @Keep.
| Configuration | Passed | Failed |
|---|---|---|
| No extra rules | 210 | 129 |
| With the six rules above | 339 | 0 |
All 129 failures go through a suspend function, and the errors are NoSuchMethodErrors like the one above. Everything else passes without the rules, including sync calls in both directions, data classes, enums and errors.
Steps to reproduce
- Write a Kotlin class with a
suspend funand annotate it with@Keep. - Generate jnigen bindings for it and call the function from Dart.
- Run
flutter run --release. The call fails with theNoSuchMethodErrorabove.
Environment
jni1.1.0 andjnigen1.0.1.kotlin.dartandconsumer-rules.proare unchanged onmain.- Flutter 3.49.0-1.0.pre-194, JDK 17, arm64 Android emulator.
- kotlin-stdlib ships no R8/ProGuard rules (checked 2.4.20). kotlinx-coroutines' bundled R8 rules don't cover these classes.
Related
- Add
Keepannotations to kotlin plugin example #640: the same error from thekotlin_pluginexample, closed in 2023 with-dontobfuscateas the workaround. That flag turns off renaming for the whole app; keeping these six classes is enough. - Generate proguard rules #681 and Inject proguard rules after Dart AOT compilation to gradle flutter/flutter#154170: generating keep rules for the classes that jnigen binds, from tree-shaking information. That's broader. This issue only covers the fixed set of Kotlin classes that jni itself depends on, which can ship now.
- The jnigen README tells apps to keep their own classes and the library classes they bind. These six are different: apps never reference them, because jni looks them up internally, so apps have no way to know they need rules.
Activity
tarrinneal commentedon Oct 6, 2026
related flutter/flutter#193880
[pigeon] Fix native interop (JNI) calls in minified Android release b…
liamappelbe commentedon Oct 7, 2026
Proposed fix SGTM. @tarrinneal I'm OOO for a few weeks, so can you make a PR with your fix and send it to Daco or Michael?
android: R8 keep rules for jnigen/Kotlin coroutines — fixes NoSuchMet…
[shared_preferences] Keep Kotlin classes used through JNI in Android …