Caching Android projects
While it is true that Android uses the Java toolchain as its foundation, there are nevertheless some significant differences from pure Java projects; these differences impact task cacheability.
This is even more true for Android projects that include Kotlin source code (and therefore use the kotlin-android
plugin).
Disambiguation
This guide is about Gradle’s build cache, but you may have also heard about the Android build cache. These are different things. The Android cache is internal to certain tasks in the Android plugin, and will eventually be removed in favor of native Gradle support.
Why use the build cache?
The build cache can significantly improve build performance for Android projects, in many cases by 30-40%. Many of the compilation and assembly tasks provided by the Android Gradle Plugin are cacheable, and more are made so with each new iteration.
Faster CI builds
CI builds benefit particularly from the build cache.
A typical CI build starts with a clean
, which means that pre-existing build outputs are deleted and none of the tasks that make up the build will be UP-TO-DATE
.
However, it is likely that many of those tasks will have been run with exactly the same inputs in a prior CI build, populating the build cache; the outputs from those prior runs can safely be reused, resulting in dramatic build performance improvements.
Reusing CI builds for local development
When you sign into work at the start of your day, it’s not unusual for your first task to be pulling the main branch and then running a build (Android Studio will probably do the latter, whether you ask it to or not). Assuming all merges to main are built on CI (a best practice!), you can expect this first local build of the day to enjoy a larger-than-typical benefit with Gradle’s remote cache. CI already built this commit — why should you re-do that work?
Switching branches
During local development, it is not uncommon to switch branches several times per day.
This defeats incremental build (i.e., UP-TO-DATE
checks), but this issue is mitigated via the use of the local build cache.
You might run a build on Branch A, which will populate the local cache.
You then switch to Branch B to conduct a code review, help a colleague, or address feedback on an open PR.
You then switch back to Branch A to continue your original work.
When you next build, all of the outputs previously built while working on Branch A can be reused from the cache, saving potentially a lot of time.
The Android Gradle Plugin
Android Studio users should use the latest Android Gradle Plugin to ensure compatibility and benefit from performance improvements in new releases.
The first thing you should always do when working to optimize your build is ensure you’re on the latest stable, supported versions of the Android Gradle Plugin and the Gradle Build Tool. At the time of writing, they are 3.3.0 and 5.0, respectively. Each new version of these tools includes many performance improvements, not least of which is to the build cache.
Java and Kotlin compilation
The discussion above in “Caching Java projects” is equally relevant here, with the caveat that, for projects that include Kotlin source code, the Kotlin compiler does not currently support compile avoidance in the way that the Java compiler does.
Annotation processors and Kotlin
The advice above for pure Java projects also applies to Android projects. However, if you are using annotation processors (such as Dagger2 or Butterknife) in conjunction with Kotlin and the kotlin-kapt plugin, you should know that before Kotlin 1.3.30 kapt was not cached by default.
You can opt into it (which is recommended) by adding the following to build scripts:
pluginManager.withPlugin("kotlin-kapt") {
configure<KaptExtension> { useBuildCache = true }
}
plugins.withId("kotlin-kapt") {
kapt.useBuildCache = true
}
Unit test execution
Like unit tests in a pure Java project, the equivalent test task in an Android project (AndroidUnitTest
) is also cacheable since Android Gradle Plugin 3.6.0.
Instrumented test execution (i.e., Espresso tests)
Android instrumented tests (DeviceProviderInstrumentTestTask
), often referred to as “Espresso” tests, are also not cacheable.
The Google Android team is also working to make such tests cacheable.
Please see this issue.
Lint
Users of Android’s Lint
task are well aware of the heavy performance penalty they pay for using it, but also know that it is indispensable for finding common issues in Android projects.
Currently, this task is not cacheable.
This task is planned to be cacheable with the release of Android Gradle Plugin 3.5.
This is another reason to always use the latest version of the Android plugin!
The Fabric Plugin and Crashlytics
The Fabric plugin, which is used to integrate the Crashlytics crash-reporting tool (among others), is very popular, yet imposes some hefty performance penalties during the build process. This is due to the need for each version of your app to have a unique identifier so that it can be identified in the Crashlytics dashboard. In practice, the default behavior of Crashlytics is to treat “each version” as synonymous with “each build”. This defeats incremental build, because each build will be unique. It also breaks the cacheability of certain tasks in the build, and for the same reason. This can be fixed by simply disabling Crashlytics in “debug” builds. You may find instructions for that in the Crashlytics documentation.
The fix described in the referenced documentation does not work directly if you are using the Kotlin DSL; see below for the workaround. |
Kotlin DSL
The fix described in the referenced documentation does not work directly if you are using the Kotlin DSL; this is due to incompatibilities between that Kotlin DSL and the Fabric plugin. There is a simple workaround for this, based on this advice from the Kotlin DSL primer.
Create a file, fabric.gradle
, in the module where you apply the io.fabric
plugin. This file (known as a script plugin), should have the following contents:
plugins.withId("com.android.application") { // or "com.android.library" android.buildTypes.debug.ext.enableCrashlytics = false }
And then, in the module’s build.gradle.kts
file, apply this script plugin:
apply(from = "fabric.gradle")