Category: Blog, Android, Development

How to Generate Proguard/R8 rules for Navigation Component Arguments

Learn how to generate Proguard/R8 rules for Android Jetpack’s Navigation component arguments.

android jetpack guide

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 the 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 the 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, if during refactoring you decide to pass something else as an argument (another class), you need to either annotate the new class and remove the annotation from the 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).

Well, I hope that the solution which I describe below will be helpful in your Android mobile app development.

Gradle to the rescue!

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

The algorithm is simple:

  • For each XML file with navigation graph:
  • For each distinct non-primitive navigation destination argument:
  • Construct Proguard rule
  • Write a rule to the file (create it if not exist)
  • Use generate file as Proguard file of the project

Note that in the case of a 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. The 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, the only default path of the 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 the 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 the 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 a cause that task will be skipped if there no input files present. However, it will still execute if the list of input files just became empty since the 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 do not contain a dot).
  4. Remove duplicates (by converting to set).
  5. For each item create a -keepnames rule.
  6. Write each rule to the 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 a custom Proguard rules path. Eg. for library project it may be:

Conclusion

The automatic generation of Proguard/R8 rules for Navigation Component destination arguments can be easily implemented with the 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!

This article was originally published at the WroclawJUG blog.

About the author

Karol Wrótniak

Karol Wrótniak

Mobile Developer

Flutter & Android Developer with 12 years of experience. A warhorse with impressive experience and skills in native and Flutter app development. Karol is probably the most active contributor to open source libraries you've ever met. He develops Gradle plugins and Bitrise steps, and he is engaged in many projects, in particular those related to testing.

Karol has been engaged as a speaker in many events and meetups like DevFest, 4Developers Wrocław, JDD Conference, Linux Academy, and more. He is an active member of Google Developers Group Wrocław, Flutter Wrocław, and Bitrise User Group.