Generate keep rules from annotations

Jetpack Keep lets you declare reflection and Java Native Interface (JNI) usage with annotations directly in your Kotlin or Java source code, instead of in separate keep rules files. The Jetpack Keep Gradle plugin reads these annotations at build time and generates the keep rules that R8 needs.

Jetpack Keep helps R8 optimization stay effective as your codebase changes:

  • Keep rules stay next to the code that needs them. When you rename or delete code that uses reflection, you update the annotation in the same place. There's no separate rules file to keep in sync.

  • Generated rules are conditional. R8 keeps the target of a reflective call only if it also keeps the code that makes the call. If the calling code is unused and R8 removes it (often the case in optional library features), R8 can also remove the target, so rules from unused features don't add bloat.

  • Lint finds reflection for you. The library includes lint checks that flag reflective calls that aren't annotated, and that can add many of the annotations automatically.

How Jetpack Keep works

Consider a function that instantiates a class by name. If R8 can't tell that your code uses the class, it removes the class's constructor, and the app crashes at runtime. To prevent the crash, you can add a rule like the following to your keep rules file:

# Unconditional: R8 keeps this constructor even if it removes the reflection that invokes it.
-keep class com.example.ResourceLocatorImpl {
    <init>();
}

With Jetpack Keep, you annotate the function that performs the reflection instead:

// ResourceLocatorImpl isn't on this module's compile classpath, so both the
// annotation and the code refer to it by name.
@UsesReflectionToConstruct(
    className = "com.example.ResourceLocatorImpl",
    // Omitting parameterTypes keeps every constructor. An empty array keeps
    // only the no-argument constructor, which the code calls.
    parameterTypes = [],
)
fun loadResourceLocator(): ResourceLocator {
    val implClass = Class.forName("com.example.ResourceLocatorImpl")
    return implClass.getDeclaredConstructor().newInstance() as ResourceLocator
}

At build time, the Gradle plugin generates a keep rule for the constructor. Unlike the preceding manually written rule, the generated rule is conditional: R8 keeps the constructor only if loadResourceLocator remains in the optimized app.

Requirements

To use Jetpack Keep, your project needs Android Gradle Plugin (AGP) 8.7 or higher.

The Gradle plugin supports the following module types:

Module type Plugin that the module applies Additional setup
Android app com.android.application None
Android library com.android.library None
Kotlin Multiplatform library with an Android target com.android.kotlin.multiplatform.library None
JVM library (Java or Kotlin) java-library or org.jetbrains.kotlin.jvm Register a JAR transform

Set up Jetpack Keep

To use Jetpack Keep, add both the library and the Gradle plugin to each module that uses reflection or JNI. The library provides the annotations and lint checks, and the plugin turns the annotations into keep rules.

  1. In your libs.versions.toml file, add the library and the plugin. The library and the plugin share the same version:

    [versions]
    # The library and the plugin ship together. Use one version for both so
    # that they stay in sync when you upgrade.
    annotationKeep = "1.0.0-alpha01"
    
    [libraries]
    androidx-annotation-keep = { group = "androidx.annotation.keep", name = "keep", version.ref = "annotationKeep" }
    
    [plugins]
    androidx-annotation-keep = { id = "androidx.annotation.keep", version.ref = "annotationKeep" }
    

    Gradle downloads the plugin from Google's Maven repository, so make sure that the pluginManagement repositories in your settings.gradle.kts file include google().

  2. In your root build.gradle.kts file, declare the plugin alongside AGP without applying it:

    plugins {
        alias(libs.plugins.android.application) apply false
        // Other plugins.
    
        // The plugin relies on AGP classes that it doesn't bundle, so declare
        // it next to AGP. Declaring it here also gives every module the same
        // plugin version.
        alias(libs.plugins.androidx.annotation.keep) apply false
    }
    
  3. In the build.gradle.kts file of each module that uses reflection or JNI, apply the plugin and add the library dependency:

    plugins {
        alias(libs.plugins.android.application)
        // The plugin reads annotations only from this module's own code, so
        // apply it in every module that uses Jetpack Keep.
        alias(libs.plugins.androidx.annotation.keep)
    }
    
    dependencies {
        implementation(libs.androidx.annotation.keep)
    }
    
  4. Sync your project with Gradle files.

For each build variant of an Android app module, the plugin registers a <variant>ExtractKeepRules task, such as releaseExtractKeepRules, and passes the generated rules to R8 for that variant. For library modules, the plugin packages the generated rules as consumer keep rules in the library's output. JVM library modules need additional setup.

Find reflection with lint

Jetpack Keep includes the following lint checks:

ReflectionWithoutKeepAnnotations
Flags reflective calls, such as Class.forName, getDeclaredConstructor, getDeclaredMethod, and getDeclaredField, that don't have a matching keep annotation.
MissingKeepAnnotationPlugin
Flags modules that depend on androidx.annotation.keep:keep but don't apply the androidx.annotation.keep Gradle plugin. Without the plugin, your build doesn't generate keep rules from the annotations.

To use lint to annotate an Android module, follow these steps:

  1. Run lint on the module to find reflective calls that need annotations:

    ./gradlew :app:lint
    
  2. Apply the fixes that lint can make automatically:

    ./gradlew :app:lintFix
    

    Lint can add an annotation automatically when it can determine which class and members the code reflects on.

  3. Review each added annotation to confirm that it matches what your code does. For example, check that the parameter types match the constructor or method that your code calls.

  4. Resolve the remaining findings in Android Studio. When lint can't determine the target class, the quick fix inserts the annotation with a TODO placeholder for you to complete. You can also add the annotation manually.

    For example, the following function receives the class name as a parameter, so lint can't determine which class it constructs. The quick fix adds an annotation with a placeholder:

    @UsesReflectionToConstruct(
        className = TODO(),
        parameterTypes = []
    )
    fun createInstance(className: String): Any =
        Class.forName(className).getDeclaredConstructor().newInstance()
    

    Replace the placeholder with the class that your code constructs. If the class is available at compile time, use classConstant instead of className. In Kotlin, your code doesn't compile until you replace TODO().

  5. Check which keep rules the plugin generates from your annotations before you remove any existing rules. For an app module, run the extraction task for the release variant:

    ./gradlew :app:releaseExtractKeepRules
    

    The plugin writes the rules to app/build/generated/release/androidx.annotation.keep.rules.pro. For library modules, see Verify the generated rules.

  6. Remove the keep rules that the new annotations replace from your keep rules files. A rule might also cover reflection that lint didn't flag, such as reflection in another module or in a library, so check each rule before you remove it:

    • For a package-wide rule, such as -keep class com.example.** { *; }, make sure that annotations or other rules cover all reflection on the package's classes.
    • For a rule that keeps specific classes or members, make sure that it closely matches one of the generated rules, or that no other reflection depends on it.
  7. Test your release build.

Annotate your code

Add a Jetpack Keep annotation to the code that performs the reflection: the function, constructor, field, or class that contains the reflective call. The annotation describes the target of the reflection, and the generated rule keeps that target only if R8 keeps the annotated code.

Choose an annotation based on what your code does:

Annotation Use when your code
@UsesReflectionToConstruct Calls a constructor through reflection, for example with getDeclaredConstructor().newInstance().
@UsesReflectionToAccessMethod Calls a method through reflection, for example with getMethod or getDeclaredMethod.
@UsesReflectionToAccessField Reads or writes a field through reflection, for example with getField or getDeclaredField.
@UnconditionallyKeep Is called from outside your app's Kotlin or Java code, for example from native code through JNI. Use this annotation only when you can't use the @UsesReflection* annotations, which generate conditional rules.

All of the @UsesReflection* annotations follow these conventions:

  • Identify the target class with classConstant or className. If the class is available at compile time, use classConstant. The compiler checks the reference, and IDE refactorings such as renaming update it. Use className only when the class isn't available at compile time, such as a class in an optional dependency. These two parameters are mutually exclusive.
  • Target the most specific class. The generated rules keep matching members in the class that you specify and in its subclasses or interface implementations. The more general the class, the more code R8 keeps.
  • Specify parameter types to keep a single overload. If you don't set parameterTypes or parameterTypeNames, the rules keep all constructors or all overloads of the method. To keep only the no-argument constructor or method, pass an empty array.
  • Repeat the annotation for multiple targets. If one function reflects on more than one class or member, add one annotation for each target.

Construct objects with reflection

Use @UsesReflectionToConstruct when your code instantiates a class through reflection. In the following example, a task runner instantiates implementations of a StartupTask interface by name. Because the annotation specifies the interface, the generated rules keep the no-argument constructor of every StartupTask implementation, as long as R8 keeps execute:

interface StartupTask {
    fun run()
}

object TaskRunner {
    // The task class is known only at runtime, so target the interface that
    // every task implements.
    @UsesReflectionToConstruct(classConstant = StartupTask::class, parameterTypes = [])
    fun execute(className: String) {
        val task = Class.forName(className)
            .getDeclaredConstructor()
            .newInstance() as StartupTask
        task.run()
    }
}

Call methods with reflection

Use @UsesReflectionToAccessMethod when your code calls a method through reflection. Set methodName to the name of the method. In the following example, the code calls a static method in an optional dependency. The class isn't available at compile time, so the annotation uses className instead of classConstant:

object CrashReporting {
    @UsesReflectionToAccessMethod(
        className = "com.example.crashlib.CrashReporter",
        methodName = "log",
        // Omitting parameterTypes keeps every log overload. Keep only the one
        // that the code calls.
        parameterTypes = [String::class],
    )
    fun log(message: String) {
        val reporterClass = try {
            Class.forName("com.example.crashlib.CrashReporter")
        } catch (e: ClassNotFoundException) {
            // Crash reporting is optional, so skip it instead of failing.
            return
        }
        reporterClass.getMethod("log", String::class.java).invoke(null, message)
    }
}

Access fields with reflection

Use @UsesReflectionToAccessField when your code reads or writes a field through reflection. Set fieldName to the name of the field:

class UserProfileFetcher {
    // User doesn't expose internalId, so reading it requires reflection.
    @UsesReflectionToAccessField(classConstant = User::class, fieldName = "internalId")
    fun readInternalId(user: User): Any? {
        val field = User::class.java.getDeclaredField("internalId")
        field.isAccessible = true
        return field.get(user)
    }
}

Keep code called from outside your app

Use @UnconditionallyKeep for code that's called from outside your app's Kotlin or Java code, such as a method that native code calls through JNI. Annotate the element that's called, not the code that calls it:

class NativeBridge {
    // Only native code calls this method, through a JNI lookup by name that
    // R8 can't see. Keep the method and its name unconditionally.
    @UnconditionallyKeep
    fun onNativeEvent(message: String) {
        Log.d("NativeBridge", "Received from native code: $message")
    }

    // The default proguard-android-optimize.txt file already keeps native
    // method names, so this method doesn't need an annotation.
    external fun startNativeProcess()
}

The generated rule keeps the annotated method and its containing class, with their original names. It doesn't keep other classes that the method's signature references. If native code depends on those classes, annotate them too, or add keep rules for them. For more information about JNI and R8, see Interaction with native code through JNI.

By default, @UnconditionallyKeep preserves the element's name. If the external caller doesn't depend on the name, set shouldPreserveName = false so that R8 can rename the element.

Verify the generated rules

This section explains how to check the generated rules for each type of module. For an app module, the plugin passes the rules directly to R8. For a library module, the plugin packages the rules as consumer keep rules in the library's output. Apps that depend on your library apply these rules automatically when they run R8, even if the apps don't use Jetpack Keep. For more information about consumer keep rules, see Optimization for library authors.

Android apps

To check which keep rules the plugin generates for an app module, run the extraction task for a build variant:

./gradlew :app:releaseExtractKeepRules

The plugin writes the rules for each variant to app/build/generated/<variant>/androidx.annotation.keep.rules.pro. A # context: comment before each rule identifies the annotated code that produced it.

To check how the generated rules affect optimization together with your other keep rules, use the R8 Configuration Analyzer.

Android libraries

Android library modules don't need any additional setup. When you build the AAR, the plugin adds the generated rules to the AAR's proguard.txt file, after any consumer keep rules that the AAR already contains. The plugin registers a <variant>KeepRulesTransformAar task for each build variant to do this.

To verify the rules, build the AAR, for example with ./gradlew :mylibrary:assembleRelease, and open proguard.txt in the generated AAR. The generated rules appear in a section with the following header:

#################################################################
#   Rules generated from androidx.annotation.keep annotations   #
#################################################################

On macOS and Linux, you can also print the rules from the command line:

unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txt

Kotlin Multiplatform libraries

Kotlin Multiplatform library modules with an Android target don't need any additional setup either. When you build the AAR for the Android target, the plugin adds the generated rules to the AAR's proguard.txt file in the same way as for Android libraries. The plugin registers an androidMainKeepRulesTransformAar task to do this.

To verify the rules, run that task and open proguard.txt in the AAR that it creates. On macOS and Linux, you can print the rules from the command line:

./gradlew :mylibrary:androidMainKeepRulesTransformAar
unzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txt

JVM libraries

Before you can verify the rules for a JVM library module, configure the module so that the plugin adds the generated rules to a copy of its JAR. Then run the transform task that you registered and check the rules files in the JAR that it creates. For example, if you named the task keepRulesJar, you can print the rules on macOS and Linux with the following commands:

./gradlew :mylibrary:keepRulesJar
unzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \
    "META-INF/com.android.tools/*" "META-INF/proguard/*"

Configure JVM libraries

For JVM library modules that produce a JAR, the plugin doesn't modify the JAR automatically. Instead, use the annotationKeep.registerJavaArchiveTransform function to register a task that creates a copy of the JAR with the generated rules, and then publish that JAR. Add the following to the library module's build.gradle.kts file:

plugins {
    id("java-library")
    alias(libs.plugins.jetbrains.kotlin.jvm)
    alias(libs.plugins.androidx.annotation.keep)
}

val jarTask = tasks.named<Jar>("jar")

// Apps run R8 on this library's code, so the generated rules must be inside
// the JAR. The plugin handles this automatically only for Android modules.
val keepRulesJarTask = annotationKeep.registerJavaArchiveTransform(
    taskName = "keepRulesJar",
    input = jarTask.flatMap { it.archiveFile },
    fileName = jarTask.flatMap { it.archiveFileName }.map { it.replace(".jar", "-keep-rules.jar") },
)

// Consumers of this library resolve apiElements and runtimeElements, so add
// the JAR that contains the generated rules to them.
configurations.configureEach {
    if (name == "apiElements" || name == "runtimeElements") {
        outgoing.artifact(keepRulesJarTask.flatMap { it.outputJar })
    }
}

dependencies {
    implementation(libs.androidx.annotation.keep)
}

The transform adds the generated rules to the JAR's targeted R8 rules location, META-INF/com.android.tools/r8/proguard.txt. If the JAR already contains rules in META-INF/com.android.tools/ or META-INF/proguard/, the transform appends the generated rules to the existing rules file instead. For more information about these locations, see Support different R8 versions.

After you register the transform, verify the rules in the JAR.

Example: Load an optional add-on library

Libraries often use reflection to load optional add-ons. For example, an analytics SDK might ship a core library and a video add-on. The add-on depends on the core library, so the core library can't refer to the add-on's classes directly. Instead, the core library looks up the add-on's class by name at runtime and turns on video analytics only if it finds the class.

Because the core library uses the add-on's class only through reflection, R8 might remove the class or its constructor from an app's release build, which makes the lookup fail. The failure happens only in release builds of apps that include the add-on, so it's hard to catch in testing.

Without Jetpack Keep, the add-on ships a consumer keep rule for its class, as shown in Reflection to support optional dependencies:

# In the add-on's consumer keep rules file.
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

This rule is in a different library from the lookup that needs it, so the two can get out of sync. With Jetpack Keep, you annotate the lookup in the core library instead:

// In the core library.
interface AnalyticsPlugin {
    fun start()
}

object AnalyticsManager {
    // The annotation and the lookup share this constant so that they can't
    // refer to different classes.
    private const val VIDEO_TRACKER_CLASS =
        "com.example.analytics.video.VideoEventTracker"

    @UsesReflectionToConstruct(
        className = VIDEO_TRACKER_CLASS,
        parameterTypes = [],
    )
    fun initialize() {
        val tracker = try {
            Class.forName(VIDEO_TRACKER_CLASS)
                .getDeclaredConstructor()
                .newInstance() as AnalyticsPlugin
        } catch (e: ClassNotFoundException) {
            // Apps that don't play video leave out the add-on, so a missing
            // class isn't an error.
            return
        }
        // This is an ordinary interface call, not a reflective one, so R8
        // keeps start() without an annotation.
        tracker.start()
    }
}

The annotation follows these guidelines:

  • Annotate the code that performs the reflection. The annotation is on initialize, which calls Class.forName, not on the add-on's class.
  • Choose the annotation based on what the code does. The lookup creates an instance of the class, so the code uses @UsesReflectionToConstruct.
  • Use className for a class that you can't reference. The add-on's class isn't on the core library's compile classpath, so the annotation can't use classConstant.
  • Keep only the constructor that the code calls. The lookup calls the no-argument constructor, so parameterTypes is an empty array.

When you build the core library's AAR, the plugin adds a rule that keeps VideoEventTracker and its no-argument constructor whenever R8 keeps initialize. Apps that use the SDK apply this rule automatically, even if they don't use Jetpack Keep, so the add-on doesn't need a rule of its own. Compared with the add-on's rule, this approach has the following advantages:

  • The rule stays next to the lookup. The core library owns both the lookup and the rule, so you update them together instead of coordinating a change across two libraries.
  • R8 can still remove unused code. If an app includes the add-on but never calls initialize, the generated rule doesn't apply, and R8 can remove VideoEventTracker. The add-on's rule keeps it in every app that includes the add-on.
  • Lint catches unannotated lookups. If the core library later adds another reflective lookup without an annotation, lint flags it, so you can fix it before you release the library.

To confirm the result, check that the generated rule appears in the AAR's proguard.txt file, as described in Android libraries. Then test a release build of an app that includes the add-on, because that's where a missing rule would break video analytics.

Additional resources