Gradle is a powerful build tool that reigns as the dominant choice among Android developers. Despite its popularity, the complexity of fine-tuned optimizations can sometimes baffle newer engineers.
One circumstance had me and my team at UPMC Enterprises scratching our heads for weeks. We needed to conditionally include or exclude entire plugins and their associated configurations from our build based on the selected type or product flavor.
Establishing a Use Case from Experience
Anyone who uses Gradle should know that most builds include at least one plugin. For Android, at least, it is common to see these plugins applied at the top of a configuration file:
In many cases, these plugins are necessary. However, my team and I encountered a plugin that triplicated our build times each time we included it in our project, regardless of its settings.
The culprit, we found, was a plugin called Dynatrace. The only way we knew we could alleviate ourselves from unnecessarily long development builds was to comment it out each time or wait an extra precious four minutes. No amount of hiding this plugin inside of `buildTypes` or `productFlavors` kept it from running.
Thus, we established our need to include or exclude code in our Gradle file conditionally.
A Fundamental Misunderstanding
Forcing a plugin and its relevant configuration to the confines of product flavor or build type may seem like the logical path forward. It certainly did to my team.
We failed to understand how Gradle operates. Before compiling any code, it evaluates each product flavor and build type as it calculates a task graph for subsequent execution during compilation. Therefore, even though we hid Dynatrace inside of only a few variants, it still ran and imported the plugin regardless of our selected build type.
Gradle Task Nomenclature
Before diving into our solution, it is essential to know how Gradle structures its variant-specific tasks. Consider this snippet:
One might run this simple command on CI machine without a second thought, but the naming convention makes it quite clear what the task is expected to do. It breaks down like this:
<task name> + (<product flavor> + <build type>) =
<task name> + <build variant>
In our project, we have several product flavors:
- `noDynatrace`
- `dynatraceDev`
- `dynatraceProd`
… and several build types:
- `debug`
- `staging`
- `release`
Therefore, I know that I can run the variant-specific task `assemble` with a `noDynatrace` flavor and in `debug` mode with `assembleNoDynatraceDebug`. The build console in Android Studio clearly shows this name.
This information supplied the final clue we needed to optimize our build time.
The Solution
The name of the task, which was assembleNoDynatraceDebug, in our case, is passed as a parameter to the Gradle script. Therefore, it is only necessary to verify the task name to make our important decision. Since Gradle configuration scripts are simply Groovy files at heart, this task became trivial.
Since we only pass a single task to our build tool, the solution was a simple as:
Conclusion
Our initial attempt to hide the offending code in a `productFlavor` or `buildType` block failed to improve our build times. We did not realize Gradle evaluated every block during the task graph calculation phase.
However, once we learned that the task name was available to us within the script, we were able to use some plain old Groovy to filter out the plugin and regain our precious four minutes.