Since the end of 2020, the Android Gradle plugin (AGP) has started to use the new version number rule. The version number will be the same as the main Gradle version, so the version after AGP 4.2 will be 7.0 (the latest version is 7.2). When you update Android Studio, you may be prompted to update Gradle to the latest available version as well. For best performance, it is recommended that you use the latest versions of Gradle and the Android Gradle plugin. The 7.0 update to The Android Gradle plugin brings with it a number of useful features. This article focuses on Gradle performance improvements, configuration caching, and plugin extensions.

If you prefer to see this in video, please click here.

Gradle performance improvements

Kotlin symbol handling optimization

Kotlin Symbol Processing (KSP) is an alternative to KAPT (Kotlin Annotation Processing Tool), which brings first-class annotation Processing capabilities to the Kotlin language. The fastest processing speed is up to twice that of KAPT. There are many well-known software libraries that provide ANNOTATION processors that are compatible with KSP, such as Room, Moshi, Kotishi, and so on. Therefore, we recommend that you migrate from KAPT to KSP as soon as possible when the various annotation processors you use in your application support KSP.

Nontransitive R class

When non-transitive r-class is enabled, the R class in your application will only contain resources declared in subprojects, and resources in dependencies will be excluded. As a result, the size of the R class in the subproject will be significantly reduced.

This change avoids recompiling downstream modules when you add new resources to runtime dependencies. In this scenario, you can achieve a 40% performance improvement for your application. In addition, we saw a 5 to 10 percent improvement in performance when cleaning the build artifacts.

You can add the following tags to the gradle.properties file:

android.nonTransitiveRClass=true
Copy the code

△ Enable non-transitive R class functionality in gradle.properties

You can also enable non-transitive R classes using refactoring tools in Android Studio Arctic Fox and above, Run Refactor –> Migrate to non-transitive R Classes on the Android Studio menu bar. This approach also helps you modify the source code if necessary. Currently, the AndroidX library has enabled this feature, so the artifacts from the AAR phase will no longer contain resources from transitive dependencies.

Lint Performance Optimization

As of Android Gradle plugin version 7.0, Lint tasks can be displayed as “up-to-date”, meaning that no Lint analysis task is required for a module if its source code and resources have not changed. You need to add options in build.gradle:

// build.gradle

android {
  ...
  lintOptions {
    checkDependencies true
  }
}
Copy the code

△ Enable Lint performance tuning in build.gradle

As a result, Lint analysis tasks can be executed in parallel across modules, making Lint tasks significantly faster.

As of version 7.1.0-Alpha 13 of The Android Gradle plugin, Lint analysis tasks are compatible with the Gradle Build cache, which can reduce the time to a new build by reusing the results of other builds:

△ Comparison of Lint times in different AGP versions

In a demo project, we turned on the Gradle build cache and set checkDependencies to true, then built using AGP 4.2, 7.0, and 7.1, respectively. As you can see from the figure above, version 7.0 was built twice as fast as 4.2; And with AGP 7.1, the speed increase is even more significant because all Lint analysis tasks hit the cache.

Not only can you get better Lint performance directly by updating the Android Gradle plugin version, but you can also configure it to further improve efficiency. One approach is to use cacheable Lint to analyze tasks. To enable Gradle’s Build Cache, you need to enable the following flags in the gradle.properties file (see Build Cache):

org.gradle.caching=true
Copy the code

△ Enable gradle build cache in gradle.properties

Another way to improve the performance of Lint’s analysis tasks is to allocate more memory to Lint if your conditions allow.

In the meantime, we recommend that you add lintOptions blocks to the Gradle configuration of your application module:

checkDependencies true
Copy the code

Add checkDependencies to build.gradle of the module

While this won’t make Lint analysis tasks perform faster, it will allow Lint to catch more problems when analyzing your specified application and generate a Lint report for the entire project.

Gradle configures the cache

Gradle build process and stages

Every time Gradle starts a build, it creates a task graph to perform the build operation. We call this process the Configuration phase, and it usually lasts from a few to tens of seconds. Gradle configuration caches can cache the output from the configuration phase and reuse these caches for subsequent builds. When the configuration cache hits, Gradle executes all tasks that need to be built in parallel. With the results of dependency resolution cached, the entire Gradle build process becomes much faster.

It is important to note that Gradle configuration caches are different from build caches, which cache the artifacts of build tasks.

△ Input of Build configuration

During the build process, your build Settings determine the outcome of the build phase. So the configuration cache captures inputs such as gradle.properties, build files, and so on into the cache. These, along with the task you requested to build, uniquely identify the task to be performed in the build.

△ Performance gains from configuring caching

The figure above shows an example of a Gradle build with 24 sub-projects using the latest versions of the Kotlin, Gradle, and Android Gradle plug-ins. We recorded before and after configuration caching was enabled in a full build, with ABI changes, and an incremental build without ABI changes. Incremental builds are done here by adding new public methods, corresponding to “ABI changed” data; Incremental builds that modify the implementation of an existing method correspond to “abI-free” data. Obviously, all three build scenarios showed a 20% speed increase.

Next, take a look at how configuration caching works with some code:

project.tasks.register("mytask", MyTask).configure {
  it.classes.from(project.configurations.getByName("compileClasspath"))
  it.name.set(project.name)
}
Copy the code

△ Example of how configuration cache works

Before the Gradle compute task execution diagram, we are still in the configuration phase. Gradle provides global objects such as Project, Task, and Configuration to create tasks with declared inputs and outputs. In the code above, we registered a task and configured it accordingly. You can see multiple uses of global objects here, such as Project.Tasks and Project.Configurations.

The process of storing the configuration cache

When all the tasks are configured, Gradle calculates the final task execution diagram based on our configuration. The configuration cache then caches the task execution graph and serializes the execution status of each task into the cache. As you can see from the figure above, all task inputs are also stored in the cache, so they must be of a specific Gradle type, or serializable data.

△ Process of loading configuration cache

Finally, when a configuration cache is hit, Gradle uses the cache entry to create a task instance. Therefore, only previously serialized state will be referenced during the execution of the newly instantiated task, and references to global state are not allowed at this stage.

△ New Build Analyzer tool panel

We added the Build Analyzer tool in the Arctic Fox version of Android Studio to help you check whether your Build is compatible with configuration caches. When your Build task is complete, open the Build Analyzer panel and you can see how long the Build configuration process just took. As shown in the figure above, the configuration build took 9.8 seconds in total. Click the Optimize This link to see more information in the new panel, as shown below:

△ Compatibility report provided by Build Analyzer

As shown, all plug-ins used in the build are compatible with configuration caching. Click “Try Configuration Cache in a build” and the IDE will update your gradle.properties file to enable Configuration caching. In cases of incomplete compatibility, Build Analyzer may also suggest that you update certain plug-ins to a new version that is compatible with the configuration cache. If your Build is incompatible with the configuration cache, the Build task will fail, and Build Analyzer provides debugging information for your reference.

An example of an incompatible configuration cache:

abstract class GetGitShaTask extends DefaultTask {
  @OutputFile File getOutputFile() { return new File(project.buildDir, "sha.txt")}@TaskAction void process() {
    def stdout = new ByteArrayOutputStream()
    project.exec {
      it.commandLine("git"."rev-parse"."HEAD")
      standardOutput = stdout
    }
    getOutputFile().write(stdout.toString())
  }
}
project.tasks.register("myTask", GetGitShaTask)
Copy the code

We have a task to calculate the current Git SHA and write the results to the output file. It runs a git command and writes the output to the given file. When we perform this build task with configuration cache enabled, two issues related to configuration cache occur:

△ Configure the contents of the cache report

When your build task is incompatible with the configuration cache, Gradle generates an HTML file with a list of problems and details. In our example, the HTML file will contain the contents of the diagram:

△ Configuration cache error report

You can find the stack trace information for each error point. Lines 5 and 11 of the build script in the example cause these problems. Looking back at the source file, you can see that the first problem is due to the use of the project.builddir method in the function that returns the location of the output file; The second problem is that the Project variable is used in TaskAction, because with configuration caching enabled, we can’t access global state at run time.

We can make some changes to the above code. To call the project.builddir method at run time, we can store the necessary information in the task properties so that it can be stored in the configuration cache together. Alternatively, we can use Gradle service injection to execute external processes and get output information. Here is the modified code for your reference:

abstract class GetGitShaTask extends DefaultTask {
  @OutputFile abstract RegularFileProperty getOutputFile()
  @javax.inject.Inject abstract ExecOperations getExecOperations()
  @TaskAction void process() {
    def stdout = new ByteArrayOutputStream()
    getExecOperations().exec {
      // ...
    }
    getOutputFile().get().asFile.write(stdout.toString())
  }
}
project.tasks.register("myTask", GetGitShaTask) {
  getOutputFile().set(
    project.layout.buildDirectory.file("sha.txt"))}Copy the code

• Use Gradle service injection to execute external processes (example of a build task compatible with configuration cache)

As you can see from the new code, we captured the location of the output file and stored it in a property during task registration, and then executed the git command and got the output of the command through the injected Gradle service. As an additional benefit of this code, since Gradle’s delay property is not evaluated until actual use, changes to buildDirectory are automatically reflected in the output file location of the task.

For more information about Gradle configuring caching and how to migrate your build tasks, see:

  • Gradle document
  • Explore the cache configuration of the Android Gradle plugin in depth

Extension of Android Gradle plugin

Many developers find that there are some actions in their build tasks that cannot be implemented directly with the Android Gradle plugin. So we’ll focus on how these capabilities can be implemented through AGP’s new Variant and Artifact apis.

Android Gradle plugin execution structure

Build types and productFlavors are concepts that can be found in the build.gradle file of your project. The Android Gradle plug-in generates different variants of objects based on these definitions for each of your build tasks. The outputs of these build tasks are registered as artifacts corresponding to the task and are divided into public and private artifacts as needed. Earlier versions of the AGP API gave you access to these build tasks, but they were not robust because the implementation details of each task changed. The Android Gradle plug-in introduces a new API in version 7.0 that gives you access to these variant objects and some intermediate artifacts. This way, developers can change the build behavior without having to manipulate the build task.

Modify artifacts produced at build time

In this section, we will add additional assets to APK by modifying the asset artifact as follows:

// buildSrc/src/main/kotlin/AddAssetTask.kt
abstract class AddAssetTask: DefaultTask() {
  @get:Input
  abstract val content: Property<String>
 
  @get:OutputDirectory
  abstract val outputDir: DirectoryProperty
 
  @TaskAction
  fun taskAction(a) {
    File(outputDir.asFile.get(), "extra.txt").writeText(content.get()}}Copy the code

Add additional assets to APK

The above code defines a task called AddAssetTask, which has only a string input content attribute and an output directory attribute (of type DirectoryProperty). This task writes the input string to a file in the output directory. We then need to write a plug-in in toyplugin.kt that uses the Variant and Artifact APIS to connect instances of AddAssetTask to the corresponding artifacts:

// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {
  override fun apply(project: Project) {
    val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
 
    androidComponents.onVariants { variant ->
      val taskProvider = 
        project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {
          it.content.set("foo")}// Core part
      variant.artifacts
        .use(taskProvider)
        .wireWith(AddAssetTask::outputDir)
        .toAppendTo(MultipleArtifact.ASSETS)
    }
  }
}
Copy the code

△ Connect the AddAssetTask instance to the corresponding artifact

The core part of the code above adds the output directory of the task to the asset directory collection and wires the task dependencies properly. We hard-coded the contents of the extra asset as “foo” in this code, but we will change this in the next steps, so keep your eyes open as you read.

Examples of intermediate artifacts for developers to work with

The figure above shows several intermediate artifacts that you can access, including ASSETS in our Toy example. The Android Gradle plug-in provides additional access to different artifacts. For example, when you want to validate the contents of an artifact, you can obtain an AAR artifact with the following code:

androidComponents.onVariants { variant ->
  val aar: RegularFileProperty = variant.artifacts.get(AAR)
}
Copy the code

△ Obtain AAR artifacts

Refer to the Android developer documentation, The Variant API, Artifacts, and Tasks, for information on the new Variants and Artifacts APIS of the Android Gradle plug-in, which will give you more insight into how to interact with intermediate artifacts.

Modify and extend the DSL

Next we need to modify the DSL of the Android Gradle plug-in to allow us to set the contents of additional assets. The new version of Android Gradle plug-in allows you to write additional DSL content for custom plug-ins, so we’ll edit additional assets for each build type in this way. The following code shows the changes we made to the build.gradle file for the module.

// app/build.gradle
 
android {
  ...
  buildTypes {
    release {
      toy 
        content = "Hello World"}}}}Copy the code

Add custom DSL in build.gradle

In addition, to be able to extend the DSL of the Android Gradle plugin, we need to create a simple interface. You can refer to the following code:

// buildSrc/src/main/kotlin/ToyExtension.kt
 
interface ToyExtension {
  var content: String?
}
Copy the code

Define the toyExtension interface

Once the interface is defined, we need to add a newly defined extension for each build type:

// buildSrc/src/main/kotlin/ToyPlugin.kt
 
abstract class ToyPlugin: Plugin<Project> {
  override fun apply(project: Project) {
    val android = project.extensions.getByType(ApplicationExtension::class.java)
 
    android.buildTypes.forEach {
      it.extensions.add("toy", ToyExtension::class.java)
    }
    // ...}}Copy the code

△ Add newly defined extensions for all build types

You can also extend product variants with custom interfaces, but we don’t need to do that in this example. We also need to make further changes to toyplugin.kt so that the plug-in can retrieve the asset content we defined in the DSL for each variant:

// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {
  override fun apply(project: Project) {
    // ...
    // Notice the omission of the previous code addition
    val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
 
    androidComponents.onVariants { variant ->
      val buildType = android.buildTypes.getByName(variant.buildType)
      val toyExtension = buildType.extensions.findByName("toy") as? ToyExtension
 
      valcontent = toyExtension? .content ? :"foo"
      val taskProvider = 
        project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {
          it.content.set(content)
        }
 
      // Notice the omission of modifying the artifact
      // ...}}}Copy the code

△ Use custom DSL in product variants

In the above code, we added a piece of code to retrieve the contents of the new toyExtension definition, which is the additional asset defined for each build type when we modified the DSL. Note that we define alternative asset content, which is the default value to use when you do not define an asset for a build type.

Use the Variant API to add custom attributes

You can also extend the Variant API in a similar way to extend the DSL, specifically by adding your own Gradle properties or a Gradle Provider to the Variant object of the Android Gradle plug-in. Extending the Variant API has several advantages over just extending DSL:

  1. DSL values are fixed, but custom variant properties can use the output of the build task, and Gradle automatically handles all build task dependencies.
  2. You can easily set separate values for custom variant properties for each variant.
  3. Custom variant properties provide simpler and more robust interactions with other plug-ins than custom DSLS.

When we need to add custom variant attributes, we first create a simple interface:

// buildSrc/src/main/kotlin/ToyVariantExtension.kt
 
interface ToyVariantExtension {
  val content: Property<String>
}
 
// Compare the previous ToyExtension (you don't need to include this part in the code)
interface ToyExtension {
  val content: String?
}
Copy the code

△ Define extensions with custom variant attributes (versus normal extensions)

By comparing this to the previous ToyExtension definition, you’ll notice that we used Property instead of nullable string types. This is done to be consistent with the code conventions within Android Gradle plug-ins, allowing you to use the output of tasks as values for custom attributes without having to worry about the complex plug-in sorting process. Other plugins can also set property values, and it doesn’t matter whether they happen before or after the Toy plugin. The following code shows how to use custom attributes:

// app/build.gradle
androidComponents {
  onVariants(
    selector().all(),
    { variant ->
      variant.getExtension(ToyVariantExtension.class)? .content ? .set("Hello ${variant.name}")})}Copy the code

Use extensions with custom variant properties in build.gradle

While this is not as simple as extending the DSL directly, it makes it easy to set the values of custom properties for each variation. Accordingly, the toyplugin.kt file needs to be modified:

// buildSrc/src/main/kotlin/ToyPlugin.kt
 
abstract class ToyPlugin: Plugin<Project> {
  override fun apply(project: Project) {
    // ...
    // Notice the omission here
    val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
 
    androidComponents.beforeVariants { variantBuilder ->
      val buildType = android.buildTypes.getByName(variantBuilder.buildType)
      val toyExtension = buildType.extensions.findByName("toy") as? ToyExtension
 
      val variantExtension = project.objects.newInstance(ToyVariantExtension::class.java)
      variantExtension.content.set(toyExtension? .content ? :"foo")
      variantBuilder.registerExtension(ToyVariantExtension::class.java, variantExtension)
 
      // Notice the omission here
      // ...}}}Copy the code

△ Register AGP extensions with custom variant attributes

In this code, we create an instance of the ToyVariable Extension, first using the value in the TOY DSL as the default value of the Property corresponding to the custom variant Property, and then registering the instance with the variant object. You will find that we used beforeVariants instead of onVariants as the variants extensions must be registered in the beforeVariants block so that other plugins in the onVariants block can use the newly registered extensions. Also note that we get the values in the custom Toy DSL in the beforeVariants block, which is actually safe. Because when the beforeVariants callback is called, the value of the DSL is treated as final and locked, there are no additional security issues. Once we get the value in the toy DSL, we assign it to the custom variant property and finally register the new extension (ToyVariantExtension) on the variant.

After completing the beforeVariants block we can continue to assign custom variants to task inputs in the onVariants block. This process is simple, please refer to the following code:

// buildSrc/src/main/kotlin/ToyPlugin.kt
 
abstract class ToyPlugin: Plugin<Project> {
  override fun apply(project: Project) {
    // ...
    // Notice the omission of the previous section
 
    androidComponents.onVariants { variant ->
      val content = variant.getExtension(VariantExtension::class.java)? .contentval taskProvider = 
        project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {
          it.content.set(content)
        }
 
      // Notice the omission of modifying the artifact
      // ...}}}Copy the code

Use custom variant attributes

The above code is a good example of the advantages of using custom variation properties, especially if you have multiple plug-ins that need to interact in a variation-specific way. If other plug-ins also want to set your custom variant properties, or use them for their build tasks, they will just need to use something similar to the onVariants code block above.

If you want to learn more about extending the Android Gradle plugin, please check out our Gradle and AGP build API series. You can also read the Android developer documentation: Extend the Android Gradle plugin or peruse the AGP Cookbook on GitHub. Stay tuned for more build and synchronization improvements in the near future.

Next step

Project Isolation

Gradle Project Isolation is a new feature based on configuration caching designed to provide faster builds and synchronization. The configuration of each project is isolated from each other and no references are allowed across projects, so Gradle can cache the sync results of each project, and whenever the build file changes, only the affected projects are reconfigured. This feature is still in development at present, you can in gradle. The properties file to add org. Gradle. Unsafe. The isolated – projects = true switch to try this feature (need gradle version 7.2 and above).

Improved Kotlin incremental compilation

We are also working with JetBrains to improve Incremental compilation for Kotlin, with the goal of supporting all incremental compilation scenarios, such as modifying Android resources, adding external dependencies, or modifying non-Kotlin upstream subprojects.

Thanks to all the developers for their support and for trying out our preview tool and providing feedback on your questions. Please continue to pay attention to our progress, and you are welcome to contact us if you have any questions.

Please click here to submit your feedback to us, or share your favorite content or questions. Your feedback is very important to us, thank you for your support!