Saying Goodbye to SNAPSHOTs with Gradle’s Composite Builds
There is a good chance that as a developer, you had to maintain a library. We all love libraries:
- They allow us to reuse code in different projects.
- They help break dependencies by concerns. Libraries (should) only tackle one problem, this means that creating libraries help us breaking our implementation into smaller problems.
- They prevent code coupling by creating one-directional relationships. It is possible for our main project to depend on a library but no the other way around.
But all these benefits don’t come for free. Managing a library can be exhausting. Let’s use the Pizza Maker
project as an example:
pizza-maker
is a Gradle/Java project. For the sake of the example, we will be focusing on its dependencies. pizza-maker
uses an internal library ingredient-validator
.
In order to maintain this library, we use semantic versioning and an artifact repository. This means that pizza-maker
’s build.gradle
contains something like this:
implementation "com.pizza:ingredient-validator:1.0.0"
Let’s say that we need to add a new method to the ingredient-validator
library to check if an ingredient is valid:
public boolean isValidPizzaIngredient(String ingredient) {
return !"pineapple".equals(ingredient);
}
Once we have changed the code in the library, the traditional approach is to change the version of our library to a new SNAPSHOT
version, 1.1.0-SNAPSHOT and release a new version of ingredient-validator
using the following command:
./gradlew release
This will release a SNAPSHOT
artifact to an external artifact repository or our local artifact repository, depending on our configuration.
Then, we will update our pizza-maker
project build.gradle
file:
implementation "com.pizza:ingredient-validator:1.1.0-SNAPSHOT"
Now we can test the integration between pizza-maker
and ingredient-validator
. Unfortunately, we didn’t consider that our string could contain characters in uppercase. We need to update our code:
public boolean isValidPizzaIngredient(String ingredient) {
return !"pineapple".equalsIgnoreCase(ingredient);
}
And repeat the process, releasing another SNAPSHOT
version of the library:
./gradlew release
After that we will recompile pizza-maker
and test the change. Once everything is working as expected, we can update the version of our library to a final 1.1.0 version and then release:
./gradlew release
Finally, we need to update pizza-maker
to use our new release version of the library:
implementation "com.pizza:ingredient-validator:1.1.0"
It took three releases, in our small example app, to ship our code. In a real-world scenario, the number of releases can be even more. This process is not technically challenging but it is error-prone and significantly reduces how fast we can ship new code. This is where Gradle’s Composite Builds comes to save the day.
Composite Builds is an incubating feature in Gradle, that allows builds to include other builds. Even better, Gradle allows us to easily switch between a traditional artifact dependency and a Composite Build on demand. Let’s see how it works!
To enable Composite Builds in our pizza-maker
we only need to open settings.gradle
and tell gradle what is the path to ingredient-validator
:
includeBuild ('../ingredient-validator')
Done! The next time that we build pizza-maker,
Gradle will use our local build of the library instead of the binary version in the artifact repository.
From now on we don’t need to release SNAPSHOT
s to see local changes in our main project. Every change in the library will be visible in the main project. Using this new functionality, we can iterate as fast as we want without worrying about which part of the code is in a library and which not.
This is a huge improvement and it gets even better. IntelliJ (including Android Studio) has native support for Composite Builds. By adding the line above, we will be able to see ingredient-validator
as a module every time we open our pizza-maker
project. This also means that we can add breakpoints in our library and debug both projects from the same IntelliJ project.
Once we are happy with our code we can then remove that line and release a final artifact.
Ok, this is great but you might be thinking “I don’t want to be adding and removing that line — it sounds error prone”.
Luckily for us, Gradle allows conditional statements in the settings.gradle
file. Using a simple if
sentence and a hidden file (.composite-ebable
) we can enable and disable Composite Builds on demand.
When the file is present, we will use composite builds:
When the file is not present, we will use the artifact repository:
We can do this just by adding the following code to our settings.gradle
file:
if (file("../ingredient-validator/.composite-enable").exists()) {
includeBuild ('../ingredient-validator')
}
This code will prevent Composite Builds from being accidentally used unless we intentionally create the .composite-enable
file in our library project.
We can easily build a small Gradle task to create this file:
task enableIngredientValidatorCompositeBuild {
group = 'Tools'
description = 'Enable Ingredient Validator composite build'
doLast {
new File("../ingredient-validator/.composite-enable").createNewFile()
}
}
And another task to delete the file:
task disableIngredientValidatorBuild {
group = 'Tools'
description = 'Disable Ingredient Validator composite build'
doLast {
File file = file("../ingredient-validator/.composite-enable")
if (file.exists()) {
file.delete()
}
}
}
Now our team can use composite builds without worrying about breaking our CI (Continuous integration) environment. While they still have the control to enable and disable Composite Builds using our new tasks:
./gradlew enableIngredientValidatorCompositeBuild
and
./gradlew disableIngredientValidatorCompositeBuild
Gradle’s Composite Builds brings the best of both worlds. It allows us to have our code decoupled and well organized in different libraries, without suffering the overhead associated with working on different codebases.
Using one IntelliJ project, instead of one per library has been a game changer. It has helped our team to move faster and it has significantly reduced the number of bugs and synchronization issues in our libraries.
Composite Builds is an essential tool to have in your Gradle arsenal. I can’t say it enough, go ahead and try it out!
With 💚 Carlos Palacin Rubio from the Groupon Android team. Special thanks to Juan Antonio, Samuel Guirado Navarro and Pratyush Kshirsagar for their help in this article.