-
Notifications
You must be signed in to change notification settings - Fork 0
Android DCE R8
The Gradle plugin generates per-function ProGuard / R8 -assumevalues rules for the generated extension functions of every local boolean flag with default = false. These rules instruct R8 to treat the flag accessor as returning a constant false at shrink time.
With the rule in place, R8:
- Constant-folds the accessor call to
false. - Determines that any branch gated on
isXxxEnabled()is unreachable. - Removes the unreachable classes and methods from the output APK or JAR.
Remote flags are excluded — their values are dynamic (fetched at runtime) and cannot be treated as constants.
The rule generation task runs automatically when you build a release variant. To run it manually:
./gradlew :app:generateFeaturedProguardRulesOutput path: app/build/featured/proguard-featured.pro
The plugin wires the generated file into R8's input automatically — no proguardFiles entry in build.gradle.kts is needed.
For a flag new_checkout with default = false, the generated rule looks like:
-assumevalues class dev.example.app.GeneratedLocalFlagsKt {
boolean isNewCheckoutEnabled(dev.androidbroadcast.featured.ConfigValues) return false;
}
R8 reads this and treats every call to configValues.isNewCheckoutEnabled() as returning false, making any if (configValues.isNewCheckoutEnabled()) { … } branch dead.
The Gradle plugin handles wiring. You do not need to:
- Add
proguardFilesentries manually. - Configure any R8 keep rules for the generated code.
- Run a separate task before the release build.
The featured-shrinker-tests module provides automated, deterministic verification that the exact rule format produced by the plugin is sufficient for R8 to perform DCE.
Why it matters: a rule that is syntactically correct but semantically wrong would silently fail to eliminate dead code. The tests catch that class of regression before it reaches consumers.
How the tests work:
-
Bytecode generation (ASM) —
SyntheticBytecodeFactorybuilds.classfiles in memory that mirror the structure the plugin generates: aConfigValuesholder, extension functions that read from it, branch-target classes (IfBranchCode,ElseBranchCode,PositiveCountCode), and a caller entry point. -
Rules generation —
ProguardRulesWriterwrites.profiles in the exact formatProguardRulesGeneratorproduces, optionally including the-assumevaluesblock. -
R8 invocation —
R8TestHarness.runR8()calls R8 programmatically viaR8Command.builder(), producing an output JAR with DCE applied.
After each run, JarAssertions inspects the output JAR and asserts which classes are present or absent.
Test scenarios:
| Module | Test | Rule | Expected result |
|---|---|---|---|
| Boolean | guarded class is eliminated when flag is assumed false |
-assumevalues … return false |
IfBranchCode absent; ElseBranchCode present |
| Boolean | guarded class survives when no assumevalues rule |
No rule | Both classes present |
| Int | guarded class eliminated when int flag assumed zero |
-assumevalues … return 0 |
PositiveCountCode absent; IntCaller present |
| Int | guarded class survives when no assumevalues rule |
No rule | Both classes present |
Run the tests:
./gradlew :featured-shrinker-tests:testOn AGP 9.x, variant.proguardFiles as a ListProperty<RegularFile> does not propagate its provider dependency to the underlying R8 task on the releases verified during the 1.0.0-Beta cycle. The plugin retains a tasks.configureEach { … } fallback inside AndroidProguardWiring.kt that explicitly establishes the task dependency. The fallback is CC-safe and will be removed when the upstream AGP gap is fixed. See Configuration Cache for full details.