---
title: Google Play DEX Code Optimization in Capacitor Apps
description: Google Play requires 25% DEX code optimization from February 2027. What a Capacitor Android app needs, including the keep rule that prevents a startup crash.
date:
  created: 2026-10-10
  updated: 2026-10-10
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
faq: true
---

# Google Play DEX Code Optimization in Capacitor Apps

If Play Console shows a "DEX code optimization" finding for your Capacitor app, the cause is the Android project template, which ships with R8 turned off. From February 2027, Google Play requires at least 25% obfuscation, optimization and shrinking for every app with more than 10 MB of DEX code, and a Capacitor app with a few dozen plugins is above that floor. This guide shows the Google Play DEX code optimization fix for Capacitor apps: two lines in `build.gradle`, a four-line `proguard-rules.pro`, and one check before you upload. The rules come from a production app with about 40 plugins on Capacitor 8.5.0 and Android Gradle Plugin 8.13.2.

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Try Capawesome Cloud free for 14 days — iOS builds without a Mac" src="https://capawesome.io/assets/banners/cloud-free-trial-capacitor-apps.png" />
  </a>
</div>

**Key takeaways:**

- From February 2027, Google Play requires at least 25% obfuscation, optimization and shrinking for apps over 10 MB of DEX code, and the Capacitor template sets `minifyEnabled false`.
- Enabling R8 with `proguard-android-optimize.txt` moves all three percentages; `@capacitor/android` already ships keep rules for plugin classes and their callback methods.
- In R8 full mode, the default since AGP 8.0, a Capacitor app crashes on startup unless `proguard-rules.pro` contains `-keep @interface com.getcapacitor.** { *; }`.
- Plugins that compile against optional backends with `compileOnly` cause "Missing class" build errors that `-dontwarn` rules resolve.
- Only a signed release build runs R8, so test on a release APK and read the percentages from `r8.json` inside the AAB before Play does.

## Play's DEX optimization requirement

Google Play measures three percentages for every uploaded app bundle, obfuscation, optimization and shrinking, and a Capacitor app built from the default template scores near zero on all three. The Android project that `npx cap add android` generates sets `minifyEnabled false` in the release build type, so R8 never touches the DEX code. In our app, Play Console reported 1% obfuscation.

The [Play Console technical quality requirements](https://support.google.com/googleplay/android-developer/answer/17492799){:target="_blank"} state that from February 2027, apps need at least 25% in each category, and that "not meeting a requirement can affect an app's visibility and publishing capabilities on Google Play". The thresholds apply to apps with more than 10 MB of DEX code and games with more than 50 MB. Capacitor, Firebase and Play Services dependencies put a plugin-heavy app over the 10 MB line. The [Android vitals page on DEX code optimization](https://developer.android.com/google/play/vitals/code-optimization){:target="_blank"} adds where the numbers come from: for apps built with AGP 8.10 or higher, Play reads them from the `r8.json` file inside the bundle. Any shrinker counts, and R8 is the one every Android project already has.

## Enable R8

Turn on R8 in `android/app/build.gradle` by setting `minifyEnabled true` in the release build type and switching the default rules file to the optimizing variant:

```groovy title="android/app/build.gradle"
android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
```

The template references `proguard-android.txt`, which contains `-dontoptimize` and blocks most of what R8 could do, and AGP 9 no longer accepts that file by default. [How to Fix Capacitor Plugin Build Errors with AGP 9](./how-to-fix-capacitor-plugin-build-errors-with-agp-9.md) covers that build failure; switching now avoids it later. R8 runs in full mode by default [since AGP 8.0](https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes){:target="_blank"}, which matters for the keep rule in the next section.

## Rules you need

A Capacitor app needs these lines in `android/app/proguard-rules.pro` on top of the rules that Capacitor, AGP and the large SDKs contribute on their own:

```proguard title="android/app/proguard-rules.pro"
# Capacitor annotations: required in R8 full mode (ionic-team/capacitor#8589)
-keep @interface com.getcapacitor.** { *; }

# Plugin-specific rules from each plugin's README
-keep class io.capawesome.capacitorjs.plugins.** { *; }

# Optional backends that plugins probe for at runtime (see missing_rules.txt)
-dontwarn io.requery.android.database.**
-dontwarn net.zetetic.database.**
```

The first rule is the one every Capacitor app needs, and the rest of this guide explains it. The second line is an example of a plugin-specific rule: most Capacitor plugins ship no consumer rules, and a plugin that relies on reflection or serialization documents its rule in its README, as the [Capacitor SQLite plugin](../../sdks/capacitor/sqlite.md) does for the `io.capawesome.capacitorjs.plugins` package. The `-dontwarn` lines depend on which plugins you use, and the "Missing class errors" section shows how to derive them.

Everything else is already covered. `@capacitor/android` declares its own `proguard-rules.pro` as consumer rules, which keep every class annotated with `@CapacitorPlugin` together with its public `@PluginMethod` methods and its `@PermissionCallback` and `@ActivityCallback` methods, even when they are private. Capacitor resolves those callbacks by method name at runtime, so permission requests and activity results survive R8 without any rule of yours. AGP's default rules keep `@JavascriptInterface` methods, which is how Capacitor's `MessageHandler` receives calls from JavaScript, and SDKs such as Firebase and Play Billing ship consumer rules of their own.

## Why the keep rule

Without the `@interface` rule, the build passes and the app crashes on startup, because Capacitor's consumer rules keep the classes annotated with `@CapacitorPlugin` but not the annotation types themselves. This is what the first launch of our minified build looked like in logcat:

```text
FATAL EXCEPTION: CapacitorPlugins
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
Caused by: java.lang.NullPointerException
    at com.getcapacitor.Plugin.getPermissionStates
    at com.getcapacitor.Plugin.checkPermissions
    at ...FirebaseMessagingPlugin.checkPermissions
```

R8 full mode strips annotations from classes that are not explicitly kept, as the Android documentation on [R8 full mode](https://developer.android.com/topic/performance/app-optimization/full-mode){:target="_blank"} states. With the `CapacitorPlugin` annotation gone, `getAnnotation(CapacitorPlugin.class)` can only return `null`, R8 removes the `pluginAnnotation` field from `PluginHandle`, and `Bridge.getPermissionStates()` dereferences `null` the first time any plugin checks its permissions. All plugins still register, because registration only needs the class. The `-keep @interface com.getcapacitor.** { *; }` rule keeps every annotation type in the package, which covers `@CapacitorPlugin`, `@Permission`, `@PermissionCallback`, `@ActivityCallback`, `@PluginMethod` and the legacy `@NativePlugin`.

Capacitor has not shipped this rule in its consumer rules yet. [Issue #8589](https://github.com/ionic-team/capacitor/issues/8589){:target="_blank"} describes the same behavior with the same workaround and is open as of this writing. Capacitor 8.5.2 merged [a null check in `getPermissionStates()`](https://github.com/ionic-team/capacitor/pull/8400){:target="_blank"}, so on 8.5.2 the build no longer crashes, but `checkPermissions()` then returns an empty permission map. The keep rule is still required on 8.5.2; the null check only hides the symptom.

Disabling full mode with `android.enableR8.fullMode=false` in `gradle.properties` also avoids the crash, at the cost of less optimization across the whole app and all its dependencies, which is the number Play measures. The keep rule is the cheaper fix.

## Missing class errors

R8 fails the build with "Missing class" errors when a plugin was compiled against a library that your app does not bundle, and some Capacitor plugins do that on purpose. The Capacitor SQLite plugin compiles against both SQLCipher (`net.zetetic.database.**`) and requery's `sqlite-android` (`io.requery.android.database.**`) as `compileOnly` dependencies and bundles only the one you select in `variables.gradle`. At runtime it probes for them with `Class.forName`, so the missing classes are expected, but R8 does not know that, and the two `-dontwarn` lines above tell it.

Whenever R8 reports missing classes, it writes a ready-to-paste file to `android/app/build/outputs/mapping/release/missing_rules.txt`. Read each entry before you paste it. A `-dontwarn` for an optional backend that the plugin checks for at runtime is correct. A `-dontwarn` for a class a plugin calls unconditionally hides a real `NoClassDefFoundError` until a user hits it, and the right fix there is adding the dependency.

## Verify locally

With the configuration above, debug builds never run R8, so the release build is the only one that shows whether the rules are complete, and it has to be signed to install. The signing properties that Android Studio injects into Gradle let you sign a local release build with the debug keystore without touching `build.gradle`:

```bash
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
adb install -r app/build/outputs/apk/release/app-release.apk
```

With the build installed, three checks cover the failure modes in this guide:

1. **Watch logcat** for `FATAL EXCEPTION`, `ClassNotFoundException` and `NoSuchMethodException` while the app starts, then run every flow that requests a permission or waits for another activity, such as the camera, a file picker or a settings screen. The annotation crash appears within the first second.
2. **Open `mapping.txt`** in `android/app/build/outputs/mapping/release/` and confirm that your plugin classes still appear under their original names. A renamed plugin class means a keep rule is missing. Archive the file with every release, since production stack traces need it.
3. **Read the Play numbers** from the bundle before you upload it: build it with `./gradlew bundleRelease` and run `unzip -p app/build/outputs/bundle/release/app-release.aab BUNDLE-METADATA/com.android.tools/r8.json`. Play Console shows the same percentages for that upload in the app bundle explorer.

## FAQ

### Why does Google Play say my Capacitor app's DEX code optimization is low?

Because the Capacitor Android template ships with `minifyEnabled false`, so R8 never runs and Play measures close to 0% obfuscation, optimization and shrinking. From February 2027, apps with more than 10 MB of DEX code need 25% in each category. Enable R8 with `proguard-android-optimize.txt` and add `-keep @interface com.getcapacitor.** { *; }` to `proguard-rules.pro`.

### Does enabling R8 break Capacitor plugins?

Most plugins keep working, because `@capacitor/android` ships consumer keep rules for plugin classes and their annotated methods. The exception in R8 full mode is the removed `@CapacitorPlugin` annotation, which breaks permission checks and is fixed with the `@interface` keep rule. Plugins that use reflection or optional dependencies may need the rules from their own README.

### Why does my Capacitor app crash on startup after enabling minifyEnabled?

R8 full mode removes the `pluginAnnotation` field from `PluginHandle` because no keep rule mentions the annotation class, so `Bridge.getPermissionStates()` dereferences `null` as soon as a plugin checks its permissions. Add `-keep @interface com.getcapacitor.** { *; }` to `android/app/proguard-rules.pro`. The issue is tracked as ionic-team/capacitor#8589.

### Do I need shrinkResources to pass the Play threshold?

No. The Play metrics measure DEX code, and `minifyEnabled true` with `proguard-android-optimize.txt` is what moves them. Resource shrinking is riskier in a Capacitor app, because resources referenced by name from JavaScript or `capacitor.config.ts`, such as the `smallIcon` of the Push Notifications plugin, are invisible to the shrinker. If you enable it, list those resources in a `res/raw/keep.xml` with `tools:keep`, as the Android guide on [customizing which resources to keep](https://developer.android.com/topic/performance/app-optimization/customize-which-resources-to-keep){:target="_blank"} describes.

### Where do I find the mapping file for Google Play and Crashlytics?

Gradle writes it to `android/app/build/outputs/mapping/release/mapping.txt`. An Android App Bundle embeds it in `BUNDLE-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 the mapping file automatically](https://firebase.google.com/docs/crashlytics/android/get-deobfuscated-reports){:target="_blank"} for obfuscated builds. The [Code shrinking with R8](../../cloud/native-builds/android-app-bundles.md#code-shrinking-with-r8) section of the Cloud docs covers the bundle side.

### How do I check the optimization percentage before uploading?

Run `unzip -p app-release.aab BUNDLE-METADATA/com.android.tools/r8.json` on the bundle. For apps built with AGP 8.10 or higher, Google Play reads its obfuscation, optimization and shrinking percentages from that file.

## Related posts

- [How to Fix Capacitor Plugin Build Errors with AGP 9](./how-to-fix-capacitor-plugin-build-errors-with-agp-9.md)
- [The Android Troubleshooting Guide for Capacitor](./troubleshooting-capacitor-android-issues.md)
- [How to Upgrade Your Capacitor App to Capacitor 8](./how-to-upgrade-your-capacitor-app-to-capacitor-8.md)
- [How to Use SQLite in a Capacitor App](./how-to-use-sqlite-in-a-capacitor-app.md)

## Try Capawesome Cloud

Capawesome Cloud runs your Gradle build as is, so a project with `minifyEnabled true` produces the minified Android App Bundle with the mapping file embedded. App Store Publishing then submits it to the Play internal testing track, where Play Console shows the new DEX code optimization score for that upload.

[Try Capawesome Cloud Free](https://capawesome.io){ .md-button .md-button--primary }

## Conclusion

Enable R8 now rather than when the February 2027 enforcement date arrives, because the first minified build of a plugin-heavy Capacitor app surfaces every missing rule at once, and you want that on an emulator instead of in a Play release. Make the release build a standing test target: run your permission and activity-result flows on it before every upload, and check `missing_rules.txt` again whenever you add a plugin. For the other Android build pitfalls Capacitor apps hit, see [The Android Troubleshooting Guide for Capacitor](./troubleshooting-capacitor-android-issues.md).

If you have any questions, join us on the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"}. To stay updated on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
