Skip to main content

Debugging 'InvokeRejected' & ProGuard Stripping Errors in Tauri v2 Android Builds

 You have spent weeks perfecting your Tauri v2 application. The Rust backend is performant, the frontend is reactive, and tauri android dev runs flawlessly on your emulator.

Then you run tauri android build to generate your signed APK or AAB for the Play Store. You install the release build, launch the app, and it crashes immediately.

Logcat spits out a generic InvokeRejectedClassNotFoundException, or a NullPointerException originating from the JNI bridge. This is the "ProGuard Trap."

If your app works in Debug but dies in Release, 99% of the time, the Android build system (Gradle + R8) has aggressively stripped out the Java/Kotlin code your Rust binary is trying to call.

Here is why this happens and exactly how to fix it in Tauri v2.

The Root Cause: JNI vs. R8 Optimization

To understand the fix, you must understand the conflict between Rust and Android's build pipeline.

1. The Invisible Bridge (JNI)

Tauri communicates between the Rust core and the Android system using the Java Native Interface (JNI). When you invoke a plugin command from Rust, it doesn't call a compiled Java function directly. Instead, it looks up the class and method by name (as a string) at runtime.

2. The Aggressive Optimizer (R8/ProGuard)

When compiling a Release build, Gradle employs R8 (the modern replacement for ProGuard). R8 performs three main tasks:

  1. Shrinking: Removes unused code.
  2. Obfuscation: Renames classes and fields (e.g., MyAuthPlugin becomes a.b).
  3. Optimization: Inlines code for performance.

3. The Disconnect

R8 analyzes your Java/Kotlin code starting from the MainActivity. It traces every method call to build a graph of "reachable" code.

However, R8 cannot read your compiled Rust binary. It does not know that your Rust code holds a string reference to com.example.plugin.MyPlugin.

Consequently, R8 assumes your plugin class is "dead code" (never called) and strips it entirely from the final APK. When Rust tries to find the class at runtime, the lookup fails, and the app crashes.

The Solution: Configuring ProGuard Rules

To fix this, we must explicitly tell R8 to keep the classes and methods that Tauri uses, even if they appear unused in the Java bytecode.

In a standard Tauri v2 project structure, you need to modify the proguard-rules.pro file located in the Android generation folder.

Step 1: Locate the Configuration File

Navigate to: src-tauri/gen/android/app/proguard-rules.pro

Note: If this file does not exist, create it in the same directory as your build.gradle.kts.

Step 2: Apply the Rules

Add the following configuration to proguard-rules.pro. This covers the core Tauri bridge and ensures your custom plugins survive minification.

# 1. Keep the main Tauri definition and annotations
# This prevents R8 from renaming the annotations we use to identify plugins
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses

# 2. Keep the Tauri Rust Bridge
# This is critical for the JNI communication layer
-keep class app.tauri.** { *; }
-keep interface app.tauri.** { *; }

# 3. Keep Custom Plugins (The Critical Fix)
# Tauri v2 plugins usually extend the Plugin class or use the @TauriPlugin annotation.
# We must ensure these classes are not renamed or stripped.

# Keep any class that is annotated with @TauriPlugin
-keep @app.tauri.annotation.TauriPlugin class * {
    *;
}

# Fallback: Keep any class that extends the base Plugin class
-keep class * extends app.tauri.plugin.Plugin {
    *;
}

# 4. Keep Data Classes (Serialization)
# If you pass JSON data from Rust to Kotlin, R8 might rename the fields 
# in your Kotlin data classes, breaking Gson/KotlinX serialization.
# Replace 'com.your.package.models' with your actual package structure.

-keep class com.yourcompany.yourapp.models.** { *; }

Step 3: Verify build.gradle.kts

Ensure your app's build.gradle.kts is actually configured to use this rules file. Open src-tauri/gen/android/app/build.gradle.kts and check the buildTypes block:

android {
    // ... other config

    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro" // Ensure this line exists
            )
        }
    }
}

Deep Dive: Handling Serialization Issues

Fixing the plugin class visibility is often only half the battle. A common secondary crash occurs during data transfer.

If your Rust code sends a JSON object to Kotlin, and you map it to a Kotlin data class, R8 might rename the fields inside that data class because they are not accessed via direct getters/setters in Java code (reflection is used during deserialization).

The Symptom: Your plugin executes, but the received arguments are null or default values.

The Fix: You must use the @Keep annotation on your Kotlin data transfer objects (DTOs).

Kotlin Implementation Example

In your plugin implementation file (e.g., ExamplePlugin.kt):

package com.example.plugin

import android.webkit.WebView
import app.tauri.annotation.Command
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
import androidx.annotation.Keep // Import this

@TauriPlugin
class ExamplePlugin(private val activity: Activity) : Plugin(activity) {

    // 1. Mark the DTO with @Keep to prevent field renaming
    @Keep
    data class UserInput(
        val username: String,
        val age: Int
    )

    @Command
    fun saveData(invoke: Invoke) {
        // If fields were renamed by R8, parsing this args object would fail silently
        val args = invoke.parseArgs(UserInput::class.java)
        
        println("Saved user: ${args.username}")
        invoke.resolve()
    }
}

Using androidx.annotation.Keep is often cleaner than writing raw ProGuard rules for every single model class. It tells R8: "Leave this class and its members exactly as they are."

Common Pitfalls and Edge Cases

1. Third-Party Libraries

If your Tauri plugin relies on a third-party Android library (e.g., a specific Biometric SDK or Payment Gateway), that library might have its own ProGuard requirements.

If the app crashes inside the third-party library code only in Release mode, search the library's documentation for "ProGuard rules" or "Consumer Rules" and add them to your proguard-rules.pro.

2. Reflection-Heavy Plugins

If you are writing a plugin that uses heavy reflection (inspecting other classes at runtime), you might encounter NoSuchMethodError. You will need to add specific rules to keep the methods you are reflecting upon:

-keep class com.example.package.ReflectedClass {
    public <methods>;
}

3. Cleaning the Build

Gradle caches build artifacts aggressively. After modifying proguard-rules.pro, always run a clean build to ensure the new rules are applied to the intermediate artifacts.

cd src-tauri/gen/android
./gradlew clean
cd ../../..
tauri android build

Conclusion

The InvokeRejected error in Tauri Android builds is almost exclusively a byproduct of the JVM's minification process clashing with Rust's JNI bridge.

By understanding that R8 treats code as "guilty until proven innocent" (unused until proven used), we can see why it strips our plugins. The fix is strictly about explicitly whitelisting the Tauri API surface and your specific data models using proguard-rules.pro and the @Keep annotation.

Do not disable isMinifyEnabled to solve this; that bloats your APK and exposes your source code. Use the targeted rules above to maintain a secure, optimized, and stable release build.