When a Flutter release build crashes at startup in a plugin's Android code while debug works, how can R8 cause it, and how do keep rules fix it?
answer
- release build type enables minify
- reflection hides uses from R8
- android/app/proguard-rules.pro picked up
- --no-shrink has no effect
- Dart code is untouched by R8
basics
~20 sFlutter's Gradle plugin enables R8 for release builds, and R8 removes or renames JVM classes it cannot see used, such as ones reached by reflection or JNI. Add -keep rules to android/app/proguard-rules.pro, which the plugin applies automatically.
solid answer
~40 sFor release builds the Flutter Gradle plugin sets `isMinifyEnabled = true`, and `isShrinkResources` for apps, adding `proguard-android-optimize.txt`, Flutter's own `flutter_proguard_rules.pro` and your `android/app/proguard-rules.pro` if it exists. R8 then strips and renames JVM classes it believes unused. Code reached only by reflection — JSON models in a native SDK a plugin wraps, classes named in native code or loaded by name — breaks only in release, typically as `ClassNotFoundException` or fields arriving `null`. Fix it with targeted `-keep` rules, or `-dontwarn` for missing optional classes, in `android/app/proguard-rules.pro`, then verify with `flutter run --release`. The `--no-shrink` flag has no effect, and R8 never touches Dart code, which is compiled ahead of time into a native library.
code
bash · 5 lines# Reproduce the release configuration, including R8, on a device
flutter run --release
# Read the crash
adb logcat | grep -E 'ClassNotFoundException|NoSuchMethodError'go deeper
Know that release builds shrink Android code with R8, so something can work in debug and break in release, and that flutter run --release reproduces it.
Explain which files the Flutter Gradle plugin feeds to R8, why reflection escapes static analysis, and that Dart code is outside R8's reach.
Diagnose release-only crashes from logcat, write narrow keep rules or -dontwarn entries, keep mapping files, and push fixes upstream to plugins.
Set release-verification practice: release-mode smoke tests in CI, plugin vetting for shipped R8 rules, and mapping-file retention per build.
## What R8 is doing in a Flutter build **R8** is the Android toolchain's code shrinker and optimiser for Java and Kotlin bytecode. It removes classes and members it believes are unused, renames what is left to short names, and optimises the rest. A Flutter app's Android side — the embedding, plugins' Android code, native SDKs they depend on — is JVM bytecode, so R8 applies to it. The Flutter Gradle plugin configures this for you. In release builds it: 1. sets `isMinifyEnabled = true` on the `release` build type (R8 on); 2. sets `isShrinkResources` for apps, removing unused Android resources; 3. adds the Android default `proguard-android-optimize.txt` rules; 4. adds Flutter's own `flutter_proguard_rules.pro` (which, among other things, carries a rule for classes implementing `FlutterPlugin` and suppresses warnings for `io.flutter.plugin.**`); 5. adds `android/app/proguard-rules.pro` **if the file exists**. The `--no-shrink` / `--shrink` option still exists on `flutter build`, but its help text says it has no effect and that code shrinking is always enabled in release builds. **Dart code is not affected.** Your Dart is compiled ahead of time into a native library by the Dart toolchain; R8 never sees it. Dart symbol obfuscation is a separate `--obfuscate` flag. ## Why "works in debug, crashes in release" Debug builds do not run R8, so everything is present under its original name. In release, R8 decides what is used by **static analysis** of bytecode. It cannot see uses that happen by name at run time: - **Reflection** — a JSON library in a native SDK that maps fields by name onto model classes; R8 renames or removes the fields and parsing yields `null` or throws. - **Class loading by name** — `Class.forName("...")` with a string from config. - **JNI** — native C/C++ code calling Java methods by name. - **Serialized or persisted names** — class names written to storage and read back. Typical symptoms in a plant-care app that wraps a Bluetooth soil-sensor SDK through a plugin: `ClassNotFoundException` or `NoSuchMethodError` in logcat at startup, or a platform-channel call returning a map with missing fields — only in `flutter run --release` and store builds. Build-time variant: R8 fails the build reporting **missing classes** referenced by a dependency for an optional feature. The fix there is either adding the missing dependency or a `-dontwarn` rule for the optional package. ## Fixing it Create or edit `android/app/proguard-rules.pro`. The Flutter Gradle plugin picks it up automatically; no Gradle edit is needed. ```proguard # Models the sensor SDK parses by reflection -keep class com.example.soilsensor.model.** { *; } # Optional dependency referenced by the SDK but not used by this app -dontwarn com.example.soilsensor.cloud.** ``` Guidelines: - **Be narrow.** Keep the packages that are reflected on, not the whole SDK, or you lose most of the size benefit. - **Prefer library-provided rules.** Well-maintained Android libraries ship consumer rules that R8 applies automatically; a crash often means an outdated plugin or SDK version whose rules are missing. - **Reproduce locally.** `flutter run --release` runs the same R8 configuration, so you can iterate without a store upload. - **Keep the mapping file.** R8 writes a mapping from obfuscated to original JVM names per build; stack traces from the store are unreadable without the matching file. | Symptom | Likely cause | Fix | |---|---|---| | `ClassNotFoundException` in release only | class loaded by name was removed | `-keep class ...` | | JSON fields `null` in release only | reflected fields renamed | `-keep` the model classes and members | | Build fails listing missing classes | optional dependency not on classpath | add the dependency or `-dontwarn` | | Crash only in Dart stack traces | not R8 (Dart is AOT-compiled) | look at Dart obfuscation or the code itself | ## When a plugin is at fault If the crash is inside a published plugin, check its changelog for fixed ProGuard or R8 rules before adding your own; the plugin should ship consumer rules for its own reflection. Adding app-level rules is the stop-gap, upgrading is the fix.
- Why doesn't passing --no-shrink to flutter build apk switch R8 off?In current Flutter the flag still parses, but its help text states it has no effect and that code shrinking is always enabled in release builds. The Flutter Gradle plugin configures minification for the release build type itself, so the fix for an R8 problem is keep rules, not disabling R8.
- Can R8 break a Flutter app's Dart code, for example by renaming a Dart class?No. Dart is compiled ahead of time into a native library, which R8 does not process. R8 only shrinks and renames JVM bytecode — the embedding, plugins' Android code and their dependencies. Dart symbol obfuscation is controlled separately by `--obfuscate`.
saying these in an interview costs you the question
- Disables R8 with --no-shrink and believes the problem is solved
- Blames R8 for a crash whose stack trace is entirely Dart
- Adds -keep class ** { *; } for the whole app
- Believes proguard-rules.pro applies only after registering it in build.gradle.kts
- Tests only debug builds before uploading a release