Gradle & Code Quality

Write Narrow R8 Keep Rules for Reflection and Serialization

A good R8 keep rule preserves the contract that static analysis cannot see without disabling optimization for an entire application. Start from the failing reflective or native call, not from a package-wide wildcard copied from a forum.

By , FounderUpdated 3 min read

Identify what must remain stable

Ask whether the runtime needs a class to exist, a member to exist, its original name, annotations, or a particular constructor. Those are separate requirements.

The keep-rule reference explains the options and modifiers. -keepnames does not necessarily prevent removal; preserving a name is different from preserving an otherwise unused class.

Prefer a narrow example

For an illustrative class loaded by its exact name:

TEXT · REFERENCE EXAMPLE
-keep,allowoptimization class com.example.plugins.ReportPlugin {
    public <init>();
}

This is appropriate only when that named class and constructor are the actual contract. It is not a universal plugin or serialization rule.

For members accessed reflectively on a class otherwise retained, a member-specific rule may be more appropriate. Verify how the object is constructed before choosing keepclassmembers.

Account for Kotlin's compiled shape

Rules match compiled classes and members, not Kotlin source syntax directly. Visibility, generated methods, suspend functions and companion objects can have bytecode forms that differ from an intuitive source-level rule.

Inspect the compiled representation or use the library's supported consumer rules. Broadly retaining every Kotlin class is not a substitute for understanding the generated API.

Let libraries own their requirements

Check whether the library already ships consumer rules. Add app-specific rules for app-specific dynamic behavior; avoid duplicating old recommendations from an unrelated library version.

Verify both correctness and remaining optimization

Build an optimized release, exercise the exact failing feature and inspect the result. A fix that retains the entire dependency graph may restore behavior while silently discarding most size and performance benefits.

Document each rule's reason and a regression test. APKLint can identify possible obfuscation and packaging clues, but only the build and runtime evidence can show that the narrow rule preserved the required contract.

Write down what the reflection call actually requires

For the ReportPlugin example above, a loader uses the exact class name and requests its public zero-argument constructor. That contract requires the class and constructor to exist and the class name to remain recognizable. It does not automatically require every private helper's original name. If the loader later discovers annotated methods, that creates an additional contract that must be evaluated explicitly.

The keep-option reference distinguishes preservation from permissions to shrink, optimize or rename. Read modifiers as part of the rule, not as decoration. A rule allowing removal cannot rescue an otherwise unreachable entry point merely because it mentions the class name.

Turn the missing contract into a narrow regression

Build an optimized test application that loads the example plugin by the production mechanism and invokes its expected behavior. Begin with the observed failing case, then add the smallest rule supported by the compiled shape. Inspect the processed output and run the test again. If the test still fails, identify whether construction, member lookup or annotation discovery failed before broadening the rule.

For a Kotlin declaration, inspect generated JVM members rather than assuming that a source-level property is one field with the same visibility. A companion, default argument or suspend function can expose a different signature. Prefer supported library consumer rules when the library owns those details; an application-specific workaround should explain why the shipped rules are insufficient.

Keep the rule near a comment describing the dynamic caller and the regression scenario. When an SDK update removes reflection or supplies corrected consumer rules, rerun the scenario without the extra application rule before deleting it. Review package-wide rules periodically by measuring what they retain and why. A successful narrow test does not justify applying the rule to unrelated serialization libraries, and a smaller APK alone does not establish semantic correctness. The desired result is a documented runtime contract that survives optimization while unrelated code remains eligible for removal or transformation.

Sources and further reading

  1. Android Developers: Add keep rules
  2. Android Developers: Enable optimization with R8

Reference review: 29 September 2026. Examples illustrate the workflow; check your installed versions, release artifact and account-specific Console requirements before applying them. This guide is not a claim that APKLint executed your project or verified your private account.

APKLint

Android inspection tools and practical release guides. About APKLint · Report a correction