Working with large projects in Gradle can be a pain. As a project grows larger over time, configuration and build times tend to grow with it. Breaking up large projects into lots of modules can help mitigate this, but unless you meticulously follow the seemingly ever-changing advice about project structure and build script configuration, it’s easy to end up with a build system that’s doing far more than it needs to. And while Gradle is doing that extra work, you have to sit and wait.
At Dropbox, like many companies, we use Gradle to build our Android apps. Our monorepo contains almost 600 projects, most of which aren’t used by our engineers on a day-to-day basis. This means that Android Studio has to load many more modules than required, and we waste time waiting for our tools.
When we’re working on features within large projects, our time is generally spent within a specific feature module. By using reasonable abstractions within our modules we are able to develop code and write tests without depending on most of the other projects in our monorepo. But wouldn’t it be great if we could easily tell Gradle exactly where we’re working and what dependencies we need, and have it ignore the rest?
To answer that question, we recently set out to find an easy way for our engineers to specify the module in which they’re working, and let Gradle and Android Studio ignore the rest.
The manual way
In the past, we’ve been able to trim the number of projects that Gradle loads by manually adjusting the projects that are included in the settings.gradle.kts file. The quick and dirty way is to simply comment out the projects that you don’t need:
include(":sample:app1")
//include(":sample:app2")
include(":sample:lib1a")
include(":sample:lib1b")
//include(":sample:lib2a")
//include(":sample:lib2b")
//include(":sample:lib2c")
include(":sample:lib-shared")
include(":sample:moved")
project(":sample:moved").projectDir = File("sample/lib-moved")
However, this approach quickly becomes unwieldy as a project grows, and the interdependency of your modules grows more complex.
Another approach is to extract all of those include("...") statements into a separate file. This way, you can more easily switch between modules by simply choosing which files you want to apply:
//apply(from = File("project-1.settings.gradle.kts")
//apply(from = File("project-2.settings.gradle.kts")
//apply(from = File("project-3.settings.gradle.kts")
apply(from = File("settings-all.gradle.kts"))
But with all of the possible permutations, it’s easy for this to become unmanageable too. On top of that, making sure those extra settings files stay in sync can be a nightmare.
Since Gradle already knows about the dependency tree of your project, wouldn’t it be great if there was an easy way to have Gradle create these files for you?
Enter Focus
Focus allows you to do just that. Our Focus Gradle Plugin evaluates your project configuration and creates a unique settings.gradle file for the module you want to focus on. This file only includes the project dependencies that a specific module requires, allowing you to easily ignore the rest. The plugin also creates a .focus file which identifies which module should currently be focused.
With these files in place, Gradle will only configure the modules you need when you sync your project. Deleting the .focus file—which can be done manually, or by using the clearFocus task—will revert to including all of your modules.
For example, while working on design systems at Dropbox, the Focus plugin allows us to easily focus on our UI Components Playground project (a sample app in our monorepo for working on design system components). This reduces the IDE sync time from 1 minute to 15 seconds.
To start, we simply run the following command:
./gradlew :applications:uicomponents_playground:focus
This creates a focus.settings.gradle file in the module’s build directory—which only includes the projects that are required to build the uicomponents_playground module—and a .focus file at the root of the project which points to the newly created Gradle settings file. Clicking the Sync Elephant icon in Android Studio will only load the required modules, improving the performance of both the IDE and Gradle.
If we want to spend some time in a different module—perhaps a dependency that needs updating—we can simply focus on that other module and sync.
./gradlew :dsys:components:focus
This way, we’re easily able to trim the number of modules loaded in the IDE and configured by Gradle. When we’re ready to go back to building the entire project, we can simply remove the .focus file in the root directory of the project, or run the following Gradle task:
./gradlew clearFocus
With Focus, our engineers are able to iterate faster throughout the day, allowing them to spend more time focused on the quality of the code they’re developing, and less time waiting for their tools to sync.
Adding the Focus plugin
Focus is a settings plugin, so it should be applied in your settings.gradle(.kts) file. Since we currently publish the plugin to Maven Central, you’ll have to add that as a repository in your pluginsManagement block:
// settings.gradle(.kts)
pluginsManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("com.dropbox.focus") version "0.4.0"
}
Next, move all of your include statements into a settings-all.gradle file:
// settings-all.gradle(.kts)
include ':sample:app2'
include ':sample:lib2c'
include ':sample:lib-shared'
// ...
include ':sample:moved'
project(':sample:moved').projectDir = new File("sample/lib-moved")
Optionally, you can configure the plugin within your settings.gradle.kts file:
// settings.gradle(.kts)
focus {
// The name of the settings file
allSettingsFileName = "settings-all.gradle" // Default
// The name of the pointer file that tells Focus which module to focus on
// This should be added to your .gitignore file.
focusFileName = ".focus" // Default
}
Lastly, don’t forget to add your Focus file—.focus by default—to your .gitignore file.
Once those steps are complete, you can use the focus Gradle tasks in each subproject to reduce the amount of time you have to sit around and wait.
Open source at Dropbox
Dropboxers who have been using the Focus plugin are incredibly happy with the results. Our Android engineers have seen significant improvements in Android Studio and Gradle performance—in some cases reducing the time it takes Android Studio to sync with Gradle from 2 minutes down to 20 seconds. We’re excited by these improvements in our build performance, and are even more excited to share these improvements with the Open Source community.
On a personal note: I’ve been involved with the Open Source community for many years, both developing and contributing to open source libraries. When I was considering where I wanted to work next, it was important to me to be a part of a company that not only values but actively participates in the open source community. One of the things that excited me about Dropbox was learning how much the company supports the use of open source libraries and also encourages Dropboxers to contribute back to the community. (If that’s something that excites you too, we’re hiring!)
When I prototyped Focus and proposed that it could become a general use plugin, Dropbox encouraged me to take the time to build it and share it with the community. While Focus isn’t quite ready for 1.0, we invite you to give it a try and let us know how it works for you. And if you run into any issues, please let us know.