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).
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:
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:
argument
nodes.argType
attribute.-keepnames
rule.
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:
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:
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!