WrocławJUG blog

How to generate Proguard/R8 rules for Navigation component arguments

The background

Android Jetpack's Navigation component is the modern solution for navigating between screens in Android apps. It supports both activities and fragments (including dialogs). What is more, it allows to pass data to destinations.

Unfortunately there is a snag. If you read documentation carefully you'll find the Proguard considerations section. So every time you use non-primitive argument types you have to remember to either annotate its classes with @Keep annotation or add corresponding -keepnames rules to Proguard/R8 configuration (of course if you don't obfuscate your code then this problem is irrelevant but most apps are obfuscated).

However, it's not a perfect solution. For example when during refactoring you decide to pass something else as an argument (another class) you need to either annotate the new class and remove annotation from previous one or update affected rules respectively. It's quite inconvenient and error prone (errors in this matter will usually be discovered at runtime of non-debug builds).

Gradle to the rescue!

It would be better if everything will work out of the box without necessity to make any additional changes manually. To achieve that we can create a buildscript task which looks for classes used as Navigation component arguments and then generates Proguard rules.

The algorithm is simple:

Note that in case of library project you need to use consumer Proguard file.

Show me the code!

In general, actions performed by buildscripts should be located inside the task. Task can be defined directly in buildscript. However, for better readability we'll use buildSrc folder. Let's create a file buildSrc/src/main/kotlin/GenerateNavArgsProguardRulesTask.kt with the skeleton: The class is declared abstract. This is not technically necessary but considered a good practice. Gradle does not instantiate task classes directly but rather creates the wrapping subclasses. Abstract modifier ensures that subclasses can be created and also prevents direct instantiations somewhere in the code.

The next step is to declare inputs and outputs: The input consists of all the navigation graph files. For simplicity, only default path of main source set is handled. We also assume that there will be no other XML files there. The output is a single file with Proguard rules inside project build directory.

Note the @InputFiles and @OutputFile annotations. If they are present the task will only run when necessary. Roughly speaking if none of inputs and outputs are changed since the latest invocation a task is assumed to be up-to-date and Gradle won't waste the time on executing it again. @SkipWhenEmpty as the name suggests cause that task will be skipped if there no input files present. However, it will still execute if list of input files just became empty since previous invocation.

Those annotations and as a consequence not executing unnecessary actions have a significant impact on build times. Especially on debug/development builds performed by developers on their local machines.

Now we know what files to read and where to save the results, so let's do the main part of the task! The algorithm is as follows:

  1. Find all argument nodes.
  2. For all these nodes take argType attribute.
  3. Filter out primitive types (assuming that their names does not contain a dot).
  4. Remove duplicates (by converting to set).
  5. For each item create a -keepnames rule.
  6. Write each rule to output file.
Note that XML parser is namespace aware.

The final touches

We can optionally set task group and description which will be displayed by Gradle (eg. in tasks command) or IDE. Additionally we can make our task cacheable. The final code looks like that:

Usage

Tasks need to be registered in order to be invoked. We can also set the dependencies so they will be executed automatically. In build.gradle.kts it may look like this: preBuild task is executed before building and already registered by Android Gradle Plugin. Don't forget to add custom Proguard rules path. Eg. for library project it may be:

Conclusion

Automatic generation of Proguard/R8 rules for Navigation Component destination arguments can be easily implemented with help of Gradle. Don't forget to properly annotate inputs and outputs of custom Gradle tasks to not hinder build process performance.

Thanks to WrocławJUG for the JDD 2019 conference ticket!

Author: Karol Wrótniak