top of page

How We Reduced Flutter App Size with R8: 48% Less DEX Code

12 minutes ago
4 min read

Most advice on reducing Flutter app size focuses on images and fonts. In our case, the biggest opportunity was Java and Kotlin code we had never written. This case study explains how we enabled R8 in our production Flutter game, resolved two release-build errors and tested the optimized build before shipping.



Flutter Android game development with R8 optimization, showing a 48% reduction in DEX code size from 66.5 MB to 34.6 MB.
How R8 optimization reduced the uncompressed Android DEX code size of the Flutter game Mythomon by 48%, from 66.5 MB to 34.6 MB. AI-generated illustration.

  

The warning that started it


Our game, Mythomon, is a Flutter monster card game with Firebase, advertising SDKs, in-app purchases and a wallet library. Google Play Console’s Android vitals flagged bad behavior for memory usage on a recent release. The app size report showed a download of about 96 MB, compared with a peer median of about 80 MB.


The app bundle explorer offered a clue: 66.5 MB of uncompressed DEX code, with code obfuscation at 4% and DEX optimization marked as low. The Java and Kotlin bytecode from our plugins and SDKs was being shipped without R8 optimization.


The cause was one line: minifyEnabled false in the release block of android/app/build.gradle. It had been there since the first commit, explicitly disabling code shrinking for our releases.



Step 1: Enable R8 in the Flutter Android release build


For our Groovy Gradle configuration, the change was:


buildTypes {
    release {
        signingConfig signingConfigs.release
        minifyEnabled true
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

R8 removes unused Java and Kotlin code, optimizes the remaining code and obfuscates names. Resource shrinking removes unused Android resources. Flutter’s Dart code is compiled separately to native code, so this change primarily affects Android-side plugins and SDKs. See Android’s official optimization guide: https://developer.android.com/topic/performance/app-optimization/enable-app-optimization



Error 1: Crashlytics requires a newer Google Services plugin


The first release build failed within 20 seconds:


Could not determine the dependencies of task :app:minifyReleaseWithR8.
> Could not create task :app:uploadCrashlyticsMappingFileRelease.
> The Crashlytics Gradle plugin 3 requires Google-Services 4.4.1 and above.

This was not a problem with R8 rules. Enabling minification triggered the Crashlytics mapping-file upload task, which requires a newer Google Services plugin than our version, 4.3.15. The mismatch had remained hidden while minification was disabled.


We fixed it by updating android/settings.gradle:


id "com.google.gms.google-services" version "4.4.2" apply false

We could have disabled mapping-file uploads, but chose not to: the mapping file allows Crashlytics to deobfuscate stack traces. The version above is the one used in our project, not a recommendation to downgrade newer projects. See Firebase’s Android Crashlytics setup guide: https://firebase.google.com/docs/crashlytics/get-started?platform=android



Error 2: R8 missing classes


The second build reached R8 and failed with errors such as:


ERROR: R8: Missing class com.amazon.privacypass.PrivacyPass
  (referenced from com.iab.omid.library.applovin...)
Missing class com.unity3d.mediation.LevelPlay
  (referenced from com.moloco.sdk...)
Missing class java.lang.management.ManagementFactory
  (referenced from io.ktor.util.debug...)

In our project, these references came from optional integrations: an advertising measurement library, a mediation SDK integration and a desktop JVM debugging helper. We determined that the corresponding code paths were not used by our Android app.


R8 generated suggested rules in build/app/outputs/mapping/release/missing_rules.txt. After reviewing the references, we added the relevant -dontwarn rules to proguard-rules.pro, documenting why each was needed:


# Optional classes referenced by ad SDKs and Ktor; not shipped
-dontwarn com.amazon.privacypass.PrivacyPass
-dontwarn com.unity3d.mediation.LevelPlay
-dontwarn java.lang.management.ManagementFactory
# ...and four more from missing_rules.txt

Caution: -dontwarn suppresses a warning; it does not supply a missing class. Only use it after confirming that the reference is safe to ignore. If the class belongs to a feature your app actually uses, add the appropriate dependency or fix the integration instead. Otherwise a successful build may still crash at runtime.



The result: 48% less uncompressed DEX code


With both fixes, the release build passed. Uncompressed DEX code fell from 66.5 MB to 34.6 MB, a reduction of 31.9 MB, or approximately 48%. We did not change any application source code.


This is a DEX measurement, not a 48% reduction in total download size. We will compare actual download size and memory metrics in Play Console after users have updated over the following weeks. We have not yet established a memory improvement.



What we tested before shipping


R8-related problems can appear at runtime in code that uses reflection, serialization, class-name lookups or SDK configuration. We installed the minified release build on an emulator and exercised the flows that touch native SDKs:


  • App launch, sign-in and initial Firebase data load

  • A complete battle through the reward screen

  • The subscription paywall and billing library initialization

  • Server-side reward claims through Cloud Functions

  • Crypto wallet creation and key generation

  • The shop, mini games and campaign map


Nothing crashed during these checks, and the logs showed no ClassNotFoundException or NoSuchMethodError. Our full suite of about 2,500 tests also passed. Flows we could not test on the emulator, such as real purchases, are priorities for Crashlytics monitoring after rollout.



Checklist: Reduce Flutter App Size with R8


  • Check build.gradle for an explicit minifyEnabled false.

  • Compare DEX size and optimization indicators in Play Console before and after.

  • Verify compatibility between Crashlytics and Google Services Gradle plugins.

  • Review missing_rules.txt and document each -dontwarn rule rather than applying it blindly.

  • Smoke-test native SDK flows using a release build, not a debug build.

  • Keep mapping-file uploads enabled for readable crash reports.

  • Monitor crashes after rollout, especially flows not covered by pre-release testing.


Our biggest lesson was that one line in a build file had been costing us for months. If your Flutter app has accumulated plugins and SDKs, the app bundle explorer is worth five minutes of your time.

bottom of page