capacitor-android-dex-optimization
About capacitor-android-dex-optimization
Guides the agent through enabling R8 in a Capacitor Android app so that it meets the Google Play DEX code optimization requirement (at least 25% obfuscation, optimization and shrinking from February 2027 for apps with more than 10 MB of DEX code). Covers `minifyEnabled true` with `proguard-android-optimize.txt` in `android/app/build.gradle`, the `-keep @interface com.getcapacitor.** { *; }` rule that prevents the startup crash in R8 full mode, per-plugin keep rules, resolving R8 'Missing class' build errors via `missing_rules.txt`, building a signed release APK with the debug keystore, and verifying with logcat, `mapping.txt` and the `r8.json` file inside the app bundle. Use when Play Console reports a DEX code optimization finding, when a Capacitor app crashes on startup after enabling `minifyEnabled`, or when R8 fails the release build. Do not use for iOS, resource shrinking, Capacitor plugin projects, Capacitor version upgrades, or non-Capacitor mobile frameworks.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ capawesome-team/ skills --skill capacitor-android-dex-optimizationAfter installing, type/capacitor-android-dex-optimizationin Claude Code to confirm it loaded.
Details
From the project READMECapacitor Android: DEX Code Optimization
Enable R8 in a Capacitor Android app and add the keep rules a Capacitor app needs so that the release build passes the Google Play DEX code optimization requirement without crashing at startup.
The Android project that npx cap add android generates sets minifyEnabled false, so R8 never runs and Google Play measures close to 0% obfuscation, optimization and shrinking. From February 2027, Google Play requires at least 25% in each category for apps with more than 10 MB of DEX code. Play reads the numbers from the r8.json file inside the app bundle for apps built with AGP 8.10 or newer.
Prerequisites
| Requirement | Version |
|---|---|
| Capacitor | 6+ |
| Android Gradle Plugin | 8.0+ (8.10+ for the r8.json check in Step 6) |
| JDK | 17 for Capacitor 6 and 7, 21 for Capacitor 8 |
adb | installed, with a device or emulator connected for Step 5 |
The project must be a Capacitor app (not a plugin) with an existing android/ directory.
Agent Behavior
- Inspect before editing. Read the current Gradle and ProGuard configuration first (Step 1). Never overwrite existing rules in
android/app/proguard-rules.pro; append to them. - Keep rules narrow. Never add
-keep class com.getcapacitor.** { *; }or-dontobfuscate. Broad rules block the optimization that Google Play measures.@capacitor/androidalready ships consumer rules for plugin classes and their annotated methods. - Only release builds run R8. Debug builds never show whether the rules are complete. Every verification step uses a signed release build.
- Read
missing_rules.txtbefore pasting it. A-dontwarnrule is correct only for a class a plugin probes for at runtime (Step 4).
Procedures
Step 1: Analyze the Project
-
Verify the project is a Capacitor app:
package.jsonlists@capacitor/androidindependenciesand theandroid/directory exists. Ifandroid/is missing, abort and inform the user. -
Read the
@capacitor/coreversion frompackage.json. Record the major version. -
Read the AGP version from
android/build.gradle:classpath 'com.android.tools.build:gradle:8.13.2'If the major version is below 8, abort and direct the user to the
capacitor-app-upgradesskill first. Record whether the version is 8.10 or newer (this decides whether Step 6 applies). -
Read the
releasebuild type inandroid/app/build.gradle. Record the values ofminifyEnabled,shrinkResources(if present) and theproguardFilesline. -
Read
android/app/proguard-rules.proand record every existing rule. -
Read
android/gradle.propertiesand record whetherandroid.enableR8.fullModeis set. If it is set tofalse, inform the user that the keep rule in Step 3 makes this setting unnecessary and offer to remove it, because full mode produces higher optimization numbers. -
Inventory the installed Capacitor plugins from
package.jsondependencies. Match these prefixes:@capacitor/*@capawesome/*and@capawesome-team/*@capacitor-community/*@capacitor-firebase/*@capacitor-mlkit/*@revenuecat/*- Any other package with
"capacitor"in itspackage.jsonkeywordsarray
Record each plugin's package name. Step 3 uses this list.
Step 2: Enable R8
In android/app/build.gradle, update the release build type. Replace minifyEnabled false with minifyEnabled true and replace proguard-android.txt with proguard-android-optimize.txt:
android {
buildTypes {
release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ minifyEnabled true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
proguard-android.txt contains -dontoptimize, which blocks most of what R8 could do, and AGP 9 no longer accepts that file by default. Do not add shrinkResources true. The Play metrics measure DEX code, and resource shrinking breaks resources that are referenced by name from JavaScript or capacitor.config.ts (for example the smallIcon of the Push Notifications plugin).
Step 3: Add Keep Rules
-
If the Capacitor version recorded in Step 1 is below 8.6.0, append the Capacitor annotation rule to
android/app/proguard-rules.pro. Skip this if the file already contains the exact rule or if the app is on Capacitor 8.6.0 or newer, which ships the rule in its consumer rules:+# Capacitor annotations: required below Capacitor 8.6.0 in R8 full mode (ionic-team/capacitor#8589) +-keep @interface com.getcapacitor.** { *; }R8 full mode (the default since AGP 8.0) strips annotations from classes that are not explicitly kept. Before Capacitor 8.6.0,
@capacitor/androidkept the classes annotated with@CapacitorPluginbut not the annotation types themselves. Without the rule, the build passes and the app crashes on startup with aNullPointerExceptionincom.getcapacitor.Plugin.getPermissionStatesthe first time a plugin checks its permissions. The rule covers@CapacitorPlugin,@Permission,@PermissionCallback,@ActivityCallback,@PluginMethodand the legacy@NativePlugin. -
For each plugin recorded in Step 1, look up its ProGuard rules and append them to
android/app/proguard-rules.proif they are missing:- Read the plugin's README in
node_modules/<package>/README.mdand search for aProguard,ProGuardorR8section. - For Capawesome, Capacitor Firebase, Capacitor MLKit and RevenueCat plugins, the
capacitor-pluginsskill ships a reference file per plugin in itsreferences/directory with the same section.
Most plugins ship no rules of their own because the Capacitor consumer rules cover them. A plugin documents a rule only when it relies on reflection or serialization. Example for the Capawesome SQLite plugin:
+# Capawesome plugins that rely on reflection +-keep class io.capawesome.capacitorjs.plugins.** { *; } - Read the plugin's README in
-
Do not add rules for
com.getcapacitor.**classes,@JavascriptInterfacemethods, Firebase or Play Services.@capacitor/androiddeclares its own consumer rules that keep every class annotated with@CapacitorPlugintogether with its@PluginMethod,@PermissionCallbackand@ActivityCallbackmethods. AGP's default rules keep@JavascriptInterfacemethods, and the large SDKs ship consumer rules of their own.
Step 4: Build a Signed Release APK
Debug builds never run R8, and a release build has to be signed to install. Sign the local release build with the Android debug keystore through the properties that Android Studio injects into Gradle. This does not change build.gradle:
cd android
./gradlew assembleRelease \
-Pandroid.injected.signing.store.file=$HOME/.android/debug.keystore \
-Pandroid.injected.signing.store.password=android \
-Pandroid.injected.signing.key.alias=androiddebugkey \
-Pandroid.injected.signing.key.password=android
If $HOME/.android/debug.keystore does not exist, run any debug build once (./gradlew assembleDebug) to create it, then retry.
If the build fails with Missing class errors, R8 writes a ready-to-paste file to android/app/build/outputs/mapping/release/missing_rules.txt. Read each entry and decide per class:
| Case | Action |
|---|---|
The plugin compiles against the library as compileOnly and probes for it at runtime with Class.forName (optional backend) | Add the -dontwarn rule to android/app/proguard-rules.pro. |
| The plugin calls the class unconditionally | Do not add -dontwarn. Add the missing dependency to android/app/build.gradle instead, or the app throws NoClassDefFoundError at runtime. |
Known optional backends:
+# Optional SQLite backends that the Capawesome SQLite plugin probes for at runtime
+-dontwarn io.requery.android.database.**
+-dontwarn net.zetetic.database.**
The Capawesome SQLite plugin compiles against both SQLCipher (net.zetetic.database.**) and requery's sqlite-android (io.requery.android.database.**) and bundles only the one selected in android/variables.gradle, so both -dontwarn lines are correct for that plugin.
Rebuild after every rule change until assembleRelease succeeds.
Step 5: Verify on a Device
-
Confirm a device or emulator is connected with
adb devices. If none is connected, hand the following checks to the user with the exact commands. -
Install the release APK:
adb install -r app/build/outputs/apk/release/app-release.apk -
Clear logcat, launch the app, and watch for the failure signatures while it starts:
adb logcat -c adb logcat | grep -E 'FATAL EXCEPTION|ClassNotFoundException|NoSuchMethodException|NullPointerException'The annotation crash from Step 3 appears within the first second. Afterwards, ask the user to run every flow that requests a permission or waits for another activity (camera, file picker, settings screen, sign-in) and watch logcat during each one.
-
Open
android/app/build/outputs/mapping/release/mapping.txtand confirm that the plugin classes recorded in Step 1 still appear under their original names. Search for the plugin's Android package, for exampleio.capawesome.capacitorjs.plugins. A renamed plugin class means a keep rule is missing. Tell the user to archivemapping.txtwith every release, since production stack traces need it.
Step 6: Read the Play Numbers Before Uploading
Skip this step if the AGP version recorded in Step 1 is below 8.10: older versions do not write r8.json, and Play computes the numbers itself.
Build the app bundle with the same signing properties as Step 4 and read the R8 metadata from it:
./gradlew bundleRelease \
-Pandroid.injected.signing.store.file=$HOME/.android/debug.keystore \
-Pandroid.injected.signing.store.password=android \
-Pandroid.injected.signing.key.alias=androiddebugkey \
-Pandroid.injected.signing.key.password=android
unzip -p app/build/outputs/bundle/release/app-release.aab BUNDLE-METADATA/com.android.tools/r8.json
The file reports the obfuscation, optimization and shrinking percentages that Play Console shows in the app bundle explorer for that upload. Each must be at least 25%. If a value is below 25%, search android/app/proguard-rules.pro for broad rules (-keep class **, -dontobfuscate, -dontoptimize, -dontshrink) and narrow them.
Step 7: Report
Summarize for the user:
- The changes to
android/app/build.gradleandandroid/app/proguard-rules.pro. - Any
-dontwarnrule added and why it is safe, and any missing dependency that was added instead. - The three percentages from
r8.json(if Step 6 ran). - That
mapping.txtmust be archived per release. An Android App Bundle embeds it inBUNDLE-METADATA/, so Play deobfuscates crash and ANR reports on its own. Only APK uploads need a manual upload in Play Console. The Firebase Crashlytics Gradle plugin uploads it automatically. - That the release build is now a standing test target: rerun Step 5 before every upload and check
missing_rules.txtagain whenever a plugin is added.
Error Handling
- App crashes on startup with
NullPointerExceptionatcom.getcapacitor.Plugin.getPermissionStates: The app is on Capacitor below 8.6.0 and the-keep @interface com.getcapacitor.** { *; }rule is missing fromandroid/app/proguard-rules.pro. Add it (Step 3) and rebuild, or upgrade to Capacitor 8.6.0, which ships the rule (ionic-team/capacitor#8646). - Build fails with
Missing class <name>: Follow themissing_rules.txtdecision table in Step 4. - Build fails because
proguard-android.txtis not supported (AGP 9): Replace it withproguard-android-optimize.txtas in Step 2. - A plugin method is not found at runtime (
NoSuchMethodException) or a plugin class is renamed inmapping.txt: Add the plugin's documented keep rule from its README (Step 3, item 2). If the README has none, add-keep class <plugin Android package>.** { *; }for that plugin only and report it to the user as a candidate for an upstream fix. adb installfails withINSTALL_PARSE_FAILED_NO_CERTIFICATES: The release APK is unsigned. RerunassembleReleasewith the four-Pandroid.injected.signing.*properties from Step 4.- Permission checks return an empty result on Capacitor 8.5.2 or 8.5.3 without crashing: Capacitor 8.5.2 added a null check in
getPermissionStates()that hides the crash but returns an empty permission map, andrequestPermissions()still throws. The keep rule from Step 3 is still required on those versions. - Resources missing at runtime after enabling
shrinkResources: Resources referenced by name from JavaScript orcapacitor.config.tsare invisible to the resource shrinker. List them inandroid/app/src/main/res/raw/keep.xmlwithtools:keep, or removeshrinkResources true.
Limitations
- The skill verifies a local release build signed with the debug keystore. The Play Console numbers are only final for the bundle that is uploaded.
- Setting
android.enableR8.fullMode=falseinandroid/gradle.propertiesalso avoids the startup crash, but it lowers the optimization across the whole app and all its dependencies. The keep rule is the cheaper fix. - Capacitor 8.6.0 ships the annotation keep rule and a rule for
@ActivityCallbackand@PermissionCallbackmethods on non-public base classes in its consumer rules. On 8.6.0 or newer, an existing-keep @interface com.getcapacitor.** { *; }line in the app is redundant but harmless. Leave it in place.
Related Skills
capacitor-app-development— General Android troubleshooting and app configuration.capacitor-plugins— Per-plugin reference files, including the ProGuard rules a plugin documents.capacitor-app-upgrades— Upgrade Capacitor and AGP to a version this skill supports.capawesome-cloud— Capawesome Cloud runs the Gradle build as is, so a project withminifyEnabled trueproduces the minified app bundle with the mapping file embedded.