[MUSIC PLAYING]
IVAN GAVRILOVIC: Hi, everyone.
I'm Ivan, a software engineer on the Android Studio Team,
and I'm joined today by Scott, who
is also an engineer on the Android Studio Team.
We're going to talk about how you can benefit from the latest
changes in Android Gradle plugin 7.0,
in order to improve your build.
We are going to talk about the performance improvements
in AGP, steps required to benefit
from Gradle configuration cache, and how
you can extend AGP using the APIs we added in 7.0.
Let's start with the performance improvements.
Kotlin Symbol Processing-- KSB--
brings first class support for annotation processing
for Kotlin, replacing KAPT.
Using KSB benefits build speed, as annotation processors
can run up to two times faster compared to KAPT.
There are already popular annotation processors
that offer KSP compatible versions, such as Room
and Moshi, with more coming.
If all of the annotation processors
that you use support KSP, make sure to migrate
from KAPT Gradle plugin to KSP Gradle plugin.
To learn more, please visit this link.
With nontransitive R classes enabled,
R class includes only the resources declared
in the sub-project itself, and none from its dependencies,
thereby reducing the size of the R class for that sub-project.
The scenario where this is very helpful
is when you add a new resource in a runtime dependency,
because we are now able to avoid recompiling downstream modules.
This incremental scenario improves
by 40% with nontransitive R classes enabled.
For clean builds, we are seeing 5% to 10% improvement.
You can add this flag in your Gradle.properties file,
but we've also added it, with factory help, in Arctic Fox.
You can run Refactor Migrate to Non-transitive R Classes
from the IDE, which adds the right flag to your
build and modify sources, if needed.
AndroidX libraries are already using this feature,
and the AER stage ship do not contain resources
from transitive dependencies anymore.
There have been several recent Lint performance improvements.
Since AGP 7.0, Lint tasks can be UP-TO-DATE,
and Lint analysis tasks can now run in parallel per module
when check dependencies is set to true.
Since AGP 7.1 alpha-13, the Lint analysis task
is compatible with the Gradle build cache.
To demonstrate the performance improvements,
we ran Lint on a project with 15 modules.
We checked dependencies set to true and Gradle caching
enabled.
As shown here, we see about a two times
improvement from AGP 4.2, with an even more dramatic
improvement in AGP 7.1 when all the Lint analysis
tasks have cache hits.
You might see Lint performance improvements
without any action on your part after updating AGP,
but there are some simple things you can try to further improve
Lint's performance.
To benefit from the Lint analysis task being cacheable,
you'll need to have the Gradle build cache enabled.
You can enable the build cache by adding this flag
to your gradle.properties file.
Please see this link for some caveats
to consider when enabling the Gradle build cache.
Another action item is to try the performance tuning
in the Android Lint user guide, including making sure you've
given Lint enough memory.
The final action item is to try setting check dependencies
to true in your application's Lint options block.
This won't make Lint run faster, but it
will allow Lint to catch more issues
and will produce a single Lint report for your entire project.
Now let's talk about Gradle configuration cache.
At the beginning of every build, Gradle
creates a task graph which is used
to execute build operations.
We call this configuration phase,
and it may take from a couple of seconds to tenths of seconds.
Configuration cache is a Gradle feature
that allows caching the output of the configuration
phase of the build, and reusing that state
in subsequent builds.
When there is a configuration cache hit,
all tasks run in parallel and dependency resolution results
are cached, resulting in further improvements.
Please note that this is different from the Gradle
build cache, which is used to cache task output.
Your build setup fully defines the result of configuration
phase of the build.
Configuration cache captures those inputs,
for example Gradle properties, and the content of your
build files.
This, together with the tasks you requested to run,
uniquely determines the tasks you will execute in the build.
To illustrate the improvements this feature provides,
let's take a look at a Gradle build
with 24 sub-projects using the latest versions of Kotlin,
Gradle, and AGP.
You can see three different scenarios-- clean
build, incremental build that adds a public method,
and incremental build that modifies method body.
In all scenarios, we are seeing 20% build speed improvement.
Let's take a closer look at how configuration cache works.
Before the task graph is computed,
we are in the configuration phase.
We are using Gradle-provided global objects,
such as project, task container, configuration container,
to create tasks with declarer inputs and outputs.
As in this example, where we register a task
and configure it.
In this script, we can see multiple usages
of global objects, like project.tasks
and project.configurations.
That's perfectly fine, as we are still configuring the build.
Once all tasks are fully configured,
Gradle computes the final task graph.
Configuration cache stores this task graph,
and it serializes the task state and saves it in cache.
All task inputs need to be of specific Gradle types
or serializable.
Finally, when there is a configuration cache hit,
stored configuration cache entries are
used to create task instances.
Only state that has been previously serialized
can be referenced in the newly instantiated tasks,
and no references to global state are allowed.
To check if your build is compatible with configuration
cache, in Arctic Fox we added a build analyzer tech.
After the build, open the build analyzer panel,
and you will see how long build configuration takes.
In this example, it is 9.8 seconds.
Click optimize this link, and it will take you
to another panel that has more detailed information.
In this particular example, all plugins are already compatible,
and clicking try configuration cache in the build
starts a flow after which my Gradle properties
file was updated, and a flag to enable configuration cache
was added.
Build analyzer may also suggest updating the plugins
to a newer version that is compatible with configuration
cache.
In case your build is incompatible with configuration
cache, build will fail, and debugging information
will be provided.
Let's take a look at the following incompatible example.
Here we have a task that computes current GitSha,
and writes it to an output file.
It runs the Git command, captures its output,
and writes the value to a file.
Running the task with configuration cache enabled
shows two configuration cache problems.
When your build is incompatible with configuration cache,
Gradle will generate an HTML file
which contains a list of issues with more information.
If we open the HTML report, we can see stack traces
pointing to the issues.
In this case they point to lines 5 and 11 in the build script.
Going back to the task source, we
can see that we have project tasks in the function that
returns the file output location.
That's the first reported issue.
The second issue is usage of project in the task action.
This is the global stage that we cannot access with
configuration cache enabled.
Let's see how we can fix this.
We can store pieces of information
that we need in task properties.
And those will be stored in the configuration cache.
Also, we can rely on Gradle-injected services
that allow us to execute external processes.
Here is a version of the task that
is compatible with configuration cache.
We captured the output location in a property
that we configured during task registration.
And the injected Gradle service is
used to launch the Git command.
This is how we can use the new task in our build.
The output location is set during task registration.
As a plus, Gradle lazy properties are live,
and any changes to the project build
directory will be automatically reflected in the output
location of the task.
To learn more about configuration cache, please
check out Gradle documentation, and this article
which describes how to migrate your build.
SCOTT POLLOM: Thanks, Ivan.
Now we'll take a look at extending the Android Gradle
plugin.
Many of you have probably wanted your
build to do something that wasn't supported
by AGP out of the box.
In this section, we'll discuss the new variant and artifact
APIs we've added, to help you modify your build safely.
Before diving into the variant and artifact APIs,
let's clarify what variant and artifact mean.
Build types and product flavors should
be familiar concepts to you from your build.gradle files.
AGP creates variant objects from these built types and product
flavors, and for each variant, AGP
produces variant-specific tasks.
The outputs of these tasks are registered as artifacts
of the corresponding variant.
Most of these intermediate artifacts are private,
but some are now public.
Previous versions of AGP allowed API access to its tasks,
but this was brittle because the tasks are an implementation
detail and subject to change.
AGP 7.0, instead, allows API access
to the variant object and some intermediate artifacts,
to allow users to influence the build's behavior
without touching the tasks.
Now we'll walk through the details of an example project,
using the new variant and artifact
APIs to modify an intermediate artifact,
add a custom element to the DSL, and add a custom
property to the variant API.
We'll be working in the build source directory
to create a custom Gradle plugin which accomplishes these goals.
The source code for this toy project
is compatible with AGP 7.1 beta 1,
and is available at this link.
First, we'll focus on modifying an intermediate artifact.
Specifically, we'll add an extra asset to the APK
by modifying the asset's artifact.
To achieve this, we'll first need a custom task
like the Add Asset Task shown here.
This task has a single string input and a single directory
property output.
The task writes the input string to a file
in the output directory.
Using the variant and artifact APIs,
we register an instance of our Add Asset Task,
and wire it to the asset's artifact.
That's all that's required to get
the extra asset into the APK.
This block of code is the key.
It adds the task's output directory
to the collection of asset directories, and wires the task
dependencies properly.
As it's written here, we're hard coding
the content of the extra asset.
But in later slides, we'll change this behavior
to allow us to set this content per variant.
AGP now allows users to interact with
several different intermediate artifacts, some of which
are shown here.
In our toy example, we appended to the assets artifact,
but AGP supports interacting with these artifacts
in several other ways.
For example, you might want to verify the contents of one
of these artifacts, which you can do by getting the artifact,
as shown here for the aar artifact.
For more ideas and examples of how
to interact with these intermediate artifacts,
please check out the new APIs and the Android Gradle plugin
blog post on Medium.
Getting back to our toy project, now we'd
like to add some DSL to allow us to set
the content of the extra asset.
AGP has recently added the ability
to add DSL for a custom plugin to AGP's existing DSL.
We'll use this feature to allow setting
the content of the extra asset per build type.
This slide shows how we will be able to use our custom
DSL in a module's build.gradle file.
Notice how the toy DSL blends in with AGP's existing DSL.
To extend APG's DSL, we first need
to create a simple interface, like the one shown here.
We can name this interface whatever we like--
we'll call it toy extension.
Once we have our interface, we can
add it to AGP's build type DSL with the code shown here.
We can extend the product flavors in a similar way,
but we won't do it in this example.
Now that we've added the custom DSL,
we want to get its value in the onVariants block,
so we can set the task input with the value.
First, we look up the custom toy extension
that we added previously, using the variant's build type.
Next we get the value from the DSL,
or use a default value if it hasn't been set in the DSL.
Finally, we register our task and set its content input
with the value from our custom DSL.
And now the extra asset will get its content
from our custom DSL.
Similar to extending the DSL, you can also now
extend the variant API by adding your own Gradle property
or provider to AGP's variant object.
But why would you want to do this?
Extending the variant API offers some advantages
over just extending the DSL.
First, unlike DSL values, custom variant properties
can be set with the output of a task,
with all task dependencies handled automatically
by Gradle.
Second, with a custom variant property,
it's easy to set it with a unique value per variant.
And third, compared to custom DSL,
custom variant properties allow for easier and more robust
interactions with other plugins.
Similar to extending the DSL, to extend the variant API,
we first need to create a simple interface,
like the one shown here.
We'll call this one toy variant extension.
Comparing it with our toy extension,
you'll notice that we're using a provider instead
of a nullable string.
This is intentional, and it's the convention
that we use internally in AGP.
Using a provider allows the value
to be set using the output of a task.
It also removes the complexity of having
to consider plug-in ordering.
Other plugins can set the value, whether they're applied
before or after our toy plugin.
This slide shows how we will be able to use our custom variant
API in a modules build.gradle file.
The usage doesn't look as clean as our custom DSL,
but here we see that setting a unique value per variant
is easier with custom variant API, compared to custom DSL.
Implementing this variant API extension in our toy plugin
will require code like this in the beforeVariants block.
In this block we're creating an instance of our toy variant
extension, setting its provider initially
with the value from our toy DSL, and then registering it
with AGP's variant object.
This must be done in the beforeVariants block, not
the onVariants block.
We cannot register variant extensions in onVariants block.
They must be registered in the beforeVariants block in order
to be available to any plugin that might access them
in the onVariants block.
Similar to a previous slide, we look up the value
from the custom toy DSL.
We can do this safely in the beforeVariants block
because the DSL is guaranteed to be finalized and locked
when the beforeVariants callback is called.
We initially set our custom variant property
with the DSL value, or default value if it's unset in the DSL.
This is an important point because it's
the same convention we use internally in AGP.
Finally, we add our custom toy variant extension
to the variant object via the register extension method.
After all that work in the beforeVariants block,
we don't have much to do in onVariants
block to set our tasks input with our custom variant
property.
This is a good illustration of why
adding such a custom variant property
is a good idea, if you have multiple Gradle plugins that
need to interact in a variant-specific way.
If another Gradle plugin wants to set your custom variant
property, or use it for one of its tasks,
then it would only need to do something similar to what's
shown in this onVariants block.
To learn more about extending AGP,
please check out this new post on the subject.
Also please take a look at the continually updated AGP
cookbook on GitHub, to see more examples.
There are more build and sync improvements on the way
in the near future.
Gradle Project Isolation is a feature
that builds on top of configuration cache,
in order to provide more build speed and sync time
improvements.
Each project is configured in isolation,
and no cross-project references are allowed.
This allows Gradle to cache sync outputs per project,
and if a build file changes, only the impacted project
is reconfigured.
The feature is currently under development.
You can try it out with this flag, using Gradle 7.2.
Please continue following Gradle release notes for more updates.
We are also working with JetBrains
to improve Kotlin incremental compilation,
with the goal of supporting all incremental scenarios.
For example, editing Android resources,
adding external dependencies, or modifying
non-Kotlin upstream sub-projects
Thanks to you, the developers, for all of your support.
Thanks for trying our preview versions,
and filing issues on our issue tracker.
Please keep up the great work and let
us know how we can help.
Thank you.
[MUSIC PLAYING]