Environment setup
Same setup as lesson 01: LiteRT-LM 0.18.0 (com.google.ai.edge.litertlm:litertlm-android:0.18.0 on Android, the LiteRTLM Swift package on iOS), Xcode 27 or Android Studio, and a real device. Mac scripts run with uv run in the litert-ship project (Python 3.11+).
What happens
Issue #3739 (opened 24 Sep 2026, still open) shows a release-only crash:
signal 6 (SIGABRT)
Abort message: 'JNI DETECTED ERROR IN APPLICATION: mid == null
in call to CallIntMethodV
from long com.google.ai.edge.litertlm.LiteRtLmJni.nativeCreateConversation(...)'
The native library looks up Java members by name through JNI, for example getTopK, getTopP, getTemperature, onMessage, onDone, and onError. With isMinifyEnabled = true, R8 sees no Kotlin code that calls them and removes them. The reporter found the AAR (0.11.0) had no proguard.txt. Debug builds are not minified, so this is invisible during development. A Google team member said they reproduced it with R8 on.
The fix
The workaround in the issue is one keep rule in your app's proguard-rules.pro:
# LiteRT-LM: native code resolves these classes and members by name through JNI.
-keep class com.google.ai.edge.litertlm.** { *; }
// app/build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}
The rule is broad. It keeps the whole package, which adds a little size. That is the safe choice until the AAR ships its own consumer rules.
Check the AAR you ship
Each new version may fix this. Look inside the AAR for consumer rules before you remove your rule:
AAR=$(find ~/.gradle/caches -name 'litertlm-android-0.18.0*.aar' | head -n 1)
unzip -l "$AAR" | grep -i -E 'proguard|consumer' || echo "no consumer rules in $AAR"
Test the real release build
Add a small instrumented test that creates a Conversation with a non-null SamplerConfig and sends one message. The issue says that path triggers the crash. Run it against the minified build, not the debug build.
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.ai.edge.litertlm.Backend
import com.google.ai.edge.litertlm.ConversationConfig
import com.google.ai.edge.litertlm.Engine
import com.google.ai.edge.litertlm.EngineConfig
import com.google.ai.edge.litertlm.SamplerConfig
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ReleaseSmokeTest {
@Test fun generatesOneReply() {
val ctx = InstrumentationRegistry.getInstrumentation().targetContext
val modelPath = ctx.filesDir.resolve("model.litertlm").absolutePath
Engine(EngineConfig(modelPath = modelPath, backend = Backend.CPU())).use { engine ->
engine.initialize()
val config = ConversationConfig(samplerConfig = SamplerConfig(topK = 1, topP = 1.0, temperature = 0.0))
engine.createConversation(config).use { conversation ->
val reply = conversation.sendMessage("Say OK.")
assertTrue(reply.toString().isNotBlank())
}
}
}
}
To run instrumented tests on a minified build, set testBuildType = "release" in the android block (or make a staging build type that copies release), and sign it with a debug key for CI. Copy the model to filesDir before the test, for example with adb push and run-as.
Sources
Key concepts
- #3739: R8 strips Kotlin members that JNI looks up by name, so a minified build aborts with
mid == null. - Debug builds are not minified, so the bug only shows in release.
- The keep rule
-keep class com.google.ai.edge.litertlm.** { *; }is the workaround from the issue.
Takeaways
- Add the keep rule now and remove it only when the AAR ships consumer rules.
- Run one smoke test on the minified build on a real device in CI.
- Use a non-null SamplerConfig in the test, because that is the path that crashed.