3-3: Variant & Artifact API v2
The evolution from Variant and Artifact API v1 to v2 brought significant changes that we're going to walk through in this section.
The source code for this section is located in the variant-api project.
3-3-1: New Packages of AGP
Starting with AGP 4.1, the Android team has divided the com.android.tools.build:gradle:$version library into two distinct parts: com.android.tools.build:gradle-api:$version and the rest of the :gradle library (henceforth referred to as :gradle and :gradle-api which are shown in Figure 3.3.1).
- The :gradle-api package: Contains public APIs available for third-party developers.
- The :gradle package: Contains internal APIs and implementation specifics, which will gradually transition into internal private classes.
As part of their long-term plans, the Android team aims to eliminate access to all internal private classes in AGP 10.0 by using Gradle Module Metadata, which offers different dependency graphs for compile and runtime, only :gradle-api artifact is available during compilation of build files. This implies that, in an ideal world, developers should depend on :gradle-api solely for Plugin development purposes. However, due to the restricted set of artifacts offered by Variant API v2, you may encounter situations that don't quite fit your use cases (based on current testing with AGP 7.2).
Simultaneously, another important changes has been rolled out since 7.0: "Classpath changes at compile time for the Android Gradle plugin." It denotes that AGP now internally utilizes implementation
configuration more (rather than api
), which could potentially remove certain artifacts from your compile classpath.
For instance, we generally refer to the com.android.Version
class to fetch the AGP version. This class stems from the com.android.tools:common:$version dependency. If we're crafting a Plugin compatible with AGP 4.2, this dependency will be indirectly transferred to our Plugin via AGP (aligning with com.android.tools:common:27.2.0). But, upon upgrading to 7.0, it must be explicitly added as a dependency, or it won't be detected. To swiftly check AGP-related dependencies, you can filter packages with group names com.android.tools
and com.android.tools.build
in the External Libraries list within your IDE. (Figure 3.3.2)
sdk-common and sdklib are two libraries that are often used for creating Ecosystem Synergy Plugins as well. A more complete statement (shown via Version Catalog) usually looks like the following:
// deps.versions.toml
[libraries]
android-gradle-plugin = { module = "com.android.tools.build:gradle",
version.ref = "agpVer" }
android-tools-sdkcommon = { module = "com.android.tools:sdk-common",
version.ref = "androidToolVer" }
android-tools-common = { module = "com.android.tools:common",
version.ref = "androidToolVer" }
android-tools-sdklib = { module = "com.android.tools:sdklib",
version.ref = "androidToolVer" }
// build.gradle.kts
dependencies {
implementation(gradleApi())
implementation(deps.kotlin.std)
compileOnly(deps.android.gradle.plugin)
compileOnly(deps.android.tools.common)
compileOnly(deps.android.tools.sdklib)
}
This snippet is extracted from the Polyfill library, detailed in Section 3-6.
3-3-2: Variant - Fetching Configured Data
With Variant API v2, you can only retrieve parsed and consolidated configuration data. It no longer supplies other information related to TaskProvider
.
val androidExtension = project.extensions
.getByType(ApplicationAndroidComponentsExtension::class.java) [I]
androidExtension.onVariants { variant -> [II]
// Configurations(Reflect the DSL models)
val mainOutput: VariantOutput = variant.outputs.single {
it.outputType == VariantOutputConfiguration.OutputType.SINGLE
} [III]
logger.lifecycle("variant name: " +
"${variant.name}")
logger.lifecycle("variant.applicationId: " +
"${variant.namespace.get()}")
logger.lifecycle("variant.versionCode: " +
"${mainOutput.versionCode.get()}")
logger.lifecycle("variant.productFlavors: " +
"${variant.productFlavors.size}")
}
At [I], the legacy Extension is swapped out for the new ApplicationAndroidComponentsExtension
. Then, at [II], we're using the Extension-specific method onVariants(...)
(one of the new lifecycle hooks) to iterate each Variant
instance, which doesn't expose the applicationVariants: DomainObjectSet<ApplicationVariant>
collection directly. The primary logic stays mostly similar to v1, but the version number and name of the final product are encapsulated within the VariantOutput
type at [III].
The intent behind replacing the Extension is simple: to isolate certain DSL fixed configuration from subsequent dynamic configuration, with the new Extension also forming part of the :gradle-api package. If you're keen on invoking the same API in the Gradle script, remember that the Extension name registered by ApplicationAndroidComponentsExtension
is androidComponents
:
// build.gradle.kts
androidComponents {
onVariants { variant ->
...
}
}
Here's a quick comparison of v1 and v2 Extensions:
v1 | v2 | |
---|---|---|
Class | com.android.build.gradle.AppExtension | com.android.build.api.variant.ApplicationAndroidComponentsExtension |
Library | :gradle | :gradle-api |
Extension Name | android | androidComponents |
Traversal Method | applicationVariants.all(...)/configureEach(...) | onVariants(...) |
There are two APIs that go with onVariants(...)
: finalizeDsl(...)
and beforeVariants(...)
as part of the new lifecycle hooks. Additionally, two more notable APIs allow developers to register and modify the Variant data structure:
fun registerExtension(
dslExtension: DslExtension,
configurator: (v: VariantExtensionConfig<VariantT>) -> VariantExtension
)
fun registerSourceType(
name: String,
)
They are not obvious in documentation but may be useful in some rare cases. More exploration can be found in subsequent sections of this book.
3-3-3: Variant - The Variant Aware Configuration
Starting with AGP 7.0, the android {}
DSL has seen significant enhancements. These changes, however, don't affect the separation between fixed and dynamic configurations when working with API v1. In API v2, you can now set up advanced configurations for a single Variant using the following Kotlin code:
androidExtension.onVariants(androidExtension.selector() // [I]
.withBuildType("release")
.withFlavor(Pair("server", "production"))
) { variant ->
val mainOutput: VariantOutput = variant.outputs.single {
// Getting the single output type
it.outputType == VariantOutputConfiguration.OutputType.SINGLE
}
mainOutput.versionName.set("1.1.0")
variant.androidResources
.aaptAdditionalParameters
.add("-v")
variant.signingConfig?.setConfig(...)
}
What's happening here? At point [I], the selector()
method in VariantManager
will return a VariantSelector
object. This object enables us to filter by buildType
and productFlavor
, and then it's fed into onVariants(...)
. This traversal process will only permit the combination of ProductionRelease
, as determined by the constraints of the VariantSelector
.
Compared to the old way of doing things with API v1, where you'd identify variants by name, this new approach is more streamlined and easier to expand upon. You can also see how the objects in the Variant now correspond to the new DSL by checking the document or related interface definition (com.android.build.api.variant.Variant
).
However, if you leap through significant version changes, such as from 4.2 to 7.0, you may notice that certain existing APIs are gone. For example, SigningConfig#setConfig(...)
did not return until AGP 7.1. Maintain vigilance over those adjustments to guarantee a seamless transition.
3-3-4: Variant - New Lifecycle
In our journey through the world of "Fetching Configured Data", we came across the use of onVariants(...)
as a new way to traverse Variants, leaving behind the old method used in Variant API v1. But hold on, this is actually a piece of a bigger puzzle known as the new lifecycle of Variant API v2, and the full story is illustrated in the Figure 3.3.3:
This intriguing new lifecycle can be broken down into three main stages. Let's roll up our sleeves and dig into what each of these stages means and what they do.
First up, the Android Extension DSL stage. This stage kicks things off, Gradle tasks the job of creating all DSL objects. It includes all the actions done by scripts and Plugins applied to the original objects of the Android Extension.
But there's a twist: the values have to be resolved during the Configuration Phase. If you take a glance back at the configuration from the beginning of Section 3-2, you'll see that these are expressed using simple data types like String
and File
. During this stage, all the alterations can accept resolved data only, which means they can not depend on any external inputs. The Lazy Configuration data type doesn't get a pass during this stage, and don't even think about fetching data from the network at this time—the time consumption during the configuration phase will make you regret it.
Once parsed, the Android Extension will be handed off to the callback method finalizeDsl(callback: (DslExtensionT) -> Unit)
. Think of this callback as your last-ditch opportunity to tweak those Android Extension DSL-related objects because, after this, they're getting locked down (Figure 3.3.4). You're free to modify configurations here within the same boundaries as the script DSL android {...}
.
androidExtension.finalizeDsl { appExt ->
appExt.buildFeatures.dataBinding = false
appExt.buildFeatures.viewBinding = true
appExt.buildTypes.getByName("debug").isDebuggable = false
appExt.productFlavors.getByName("staging").minSdk = 30
}
Part 2, the VariantBuilder
stage. Here, AGP takes the Android Extension DSL configuration from the earlier step and crafts the VariantBuilder. Think of the VariantBuilder as a bridge between the DSL objects and the finalized Variant object. It's an intermediate state where you have the opportunity to make some last-minute tweaks.
Let's start with how the combined VariantBuilder
is handled. First, it's fed into the method below:
fun beforeVariants(selector: VariantSelector = selector().all(),
callback: (VariantBuilderT) -> Unit)
This is your last call to make changes to the VariantBuilder
's associated objects before they head over to the VariantFactory. Well, don't expect too much freedom here. The alterations you can make are pretty specific, mainly limited to different versions of the Android SDK. (Figure 3.3.5)
The most vital API here is to disable unnecessary combinations and register Variant extensions. Remember those introductions about the same operation in API v1? That was all about making the Configuration Phase run faster. Well, API v2 decided to make things a bit more streamlined by placing this functionality into the beforeVariants(...)
stage. So now, developers have an VariantBuilder#enabled
switch at their disposal. And if you want to filter out some Variants, you can do that too, with the help of our old friend the VariantSelector
pattern.
Here's a little code snippet to show you how you might put that VariantBuilder#enabled
switch to use:
androidExtension.beforeVariants(androidExtension.selector()
.withBuildType("debug")
.withFlavor(Pair("server", "production"))) { variantBuilder ->
variantBuilder.enabled = false // Turning off the variant here
}
So far, all the Extensions we've encountered come bundled with the Android Gradle Plugin. But did you know that during the VariantBuilder
stage, AGP offers an option for custom Extensions? This feature brings in additional extensions and hook them onto the resulting Variant object. How is this magic performed? Through the fun <T: Any> registerExtension(type: Class<out T>, instance: T)
method of VariantBuilder
.
Think of it as a way to supplement your Variants with extra configuration data or some handy tools. Since we're at the VariantBuilder
stage, the classes you're providing can't be fully Variant Aware yet. You'll have to limit yourself to what the VariantBuilder
can offer, like the Variant name. Here's an example of how to create a tool to generate a Task name suffix based on the Variant name.
class NameGenerator(private val variantName: String) {
val taskNameSuffix = "V2Based" + variantName.capitalize()
}
...
androidExtension.beforeVariants { variantBuilder ->
variantBuilder.minSdk = 30
variantBuilder.registerExtension( // [I]
NameGenerator::class.java,
NameGenerator(variantBuilder.name)
)
}
androidExtension.onVariants { variant ->
variant.getExtension(NameGenerator::class.java)!!
.taskNameSuffix // [II]
...
}
We first registered the Extension at [I] and grab it back at [II]. In contrast to a Kotlin Extension or a static function, the Variant Extension attaches the lifecycle to the Variant, defining a more precise scope.
Ultimately, moving on to part three, the Variant
phase. Those Variant
s whipped up by the VariantFactory
are about to be sent through the fun onVariants(selector: VariantSelector = selector().all(), callback: (VariantT) -> Unit)
method. This is your last shot, the final opportunity to tweak the Variant
objects before they're locked in place.
The configurations you're allowed to modify rely on Lazy Configuration data types (see Section 4-2), meaning you're working with various Provider
/Property
types. Here's a bit of code:
interface Variant : Component, HasAndroidResources {
val namespace: Provider<String>
val buildConfigFields: MapProperty<String, BuildConfigField<out Serializable>>
...
}
Below is a sneak peek at some of the configuration access methods similar to API v1:
androidExtension.onVariants { variant ->
// Configurations
logger.lifecycle("variant name: ${variant.name}")
logger.lifecycle("variant.applicationId: "
+ "${variant.namespace.get()}")
logger.lifecycle("variant.versionCode: "
+ "${mainOutput.versionCode.get()}")
logger.lifecycle("variant.productFlavors: "
+ "${variant.productFlavors.size}")
}
Developers can also create Variant-Aware Tasks here, perform additional Task configurations, etc., which we have already shown in Sections 2-4 and 3-2, and will not repeat here.
3-3-5: Artifact - Acquisition
The new Artifact API introduced in AGP 7.0, brings a rich set of 10 artifacts at your fingertips, ready to be accessed or altered. It's a significant upgrade from API v1, which limited us to just APK and AAR products. To give you a clear view of what's new in the inventory, here's a neat table showcasing the 11 artifacts:
(Alias SA=SingleArtifact, MA=MultipleArtifact)
Artifact | v2 |
---|---|
SA.AAR | Gets the final AAR file of the Library |
SA.APK | Gets the folder containing the final APK(s) of the Application |
SA.BUNDLE | Gets the final AAB file of the Application |
SA.MERGED_MANIFEST | Gets the merged AndroidManifest.xml file |
SA.METADATA_LIBRARY_DEPENDENCIES_REPORT | Gets the metadata of the Library dependencies |
SA.OBFUSCATION_MAPPING_FILE | Gets the obfuscation mapping.txt |
SA.PUBLIC_ANDROID_RESOURCES_LIST | Gets the publish.xml file of the Library (used to specify required classes in main DEX file) |
MA.ALL_CLASSES_DIRS | Gets source folders of all classes for DEX (removed in AGP 8.0, see Section 7-3) |
MA.ALL_CLASSES_JARS | Gets JAR packages of all classes for DEX (removed in AGP 8.0, see Section 7-3) |
MA.ASSETS | Gets all Assets for APK or Bundle |
MA.MULTIDEX_KEEP_PROGUARD | Gets multiDexKeepProguard configuration file |
Playing with these requires some familiarity with Lazy Configuration data types. Let's touch on this fascinating concept.
You've got org.gradle.api.provider.Provider<T>
, which has been around since Gradle 4.0. It's a lazy evaluation interface, akin to Java 8's java.util.function.Supplier<T>
or Dagger2's dagger.Lazy<T>
interface. The magic here? It waits until the last possible moment (when you call get()
) to perform computations. This helps speed things up during configuration and tackles the data transmission between Tasks (remember Section 3-2).
But that's not all. Since the data in Provider<T>
is immutable, its subclass Property<T>
usually comes into play when you need to declare settable data types. Plus, Gradle 4.1 added two new file containers, RegularFile
and Directory
to make file handling more explicit. These can be used in conjunction with Property<T>
to create RegularFileProperty
and DirectoryProperty
. You'll be seeing these types a lot throughout the book.
Now, back to the main show - Artifact API v2. Going back to our classic example, let's rename an APK:
androidExtension.onVariants{ variant ->
val renameApkTask = project.tasks.register(
"rename${variantCapitalizedName}Apk",
RenameApkFile::class.java
) {
val apkFolderProvider = variant.artifacts [I]
.get(SingleArtifact.APK)
this.apkFolder.set(apkFolderProvider)
this.builtArtifactsLoader.set(
variant.artifacts.getBuiltArtifactsLoader()) [II]
this.outApk.set(File(apkFolderProvider.get().asFile,
"custom-name-${variant.name}.apk")) [IV]
}
}
...
abstract class RenameApkFile : DefaultTask() {
@get:InputFiles
abstract val apkFolder: DirectoryProperty
@get:Internal
abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>
@get:OutputFile
abstract val outApk: RegularFileProperty
@TaskAction
fun taskAction() {
val builtArtifacts = builtArtifactsLoader.get()
.load(apkFolder.get()) [III]
?: throw RuntimeException("Cannot load APKs")
File(builtArtifacts.elements.single().outputFile)
.copyTo(outApk.get().asFile)
}
}
Here's what's happening: At [I], we're using one of the API v2 methods to grab the Artifact, represented by:
fun <FileTypeT: FileSystemLocation> get(
type: SingleArtifact<FileTypeT>
): Provider<FileTypeT>
Its counterpart for multiple artifacts is:
fun <FileTypeT: FileSystemLocation> getAll(
type: MultipleArtifact<FileTypeT>
): Provider<List<FileTypeT>>
Notice that what we're getting at [I] isn't the APK file itself, but the Provider
of the folder containing the APK. We then use BuiltArtifactsLoader
at [II] to find the actual APK file inside at [III]. The utilization of functional decomposition facilitates the reusability of tools. In this context, the BuiltArtifactsLoader
can be employed in various scenarios, including the retrieval of AAR files. Additionally, when modifying the Variant configuration, the resulting set of APKs resembles the VariantOutput
. This distinction is relevant in differentiating between scenarios involving a single product and those involving multiple products, such as enabling the split-apk functionality to generate multiple APKs.
You might have noticed the absence of a Task dependency in the Task registration process. More precisely, we left out the dependsOn(variant.packageApplicationProvider)
line, which you might remember from the same Task in API v1. There's more to it than just a missing line, though.
When registering a Task, the returned type TaskProvider<T>
is indeed an implementation of Provider
, as demonstrated below:
val renameApkTask: TaskProvider<RenameApkFile> = project.tasks
.register<RenameApkFile>(...)
Let's explore how this works. Imagine you have TaskProvider
A, and you're creating a new Provider
B through a transformation process. (If you want a refresher on map(...)
and flatmap(...)
, check out Section 4-4.) This action makes A an implicit dependency of Provider B. Then, when B is used as an @Input
property for TaskProvider
C, C will now automatically depend on Task A.
Here's a concrete example: consider retrieving the APK folder via variant.artifacts.get(SingleArtifact.APK)
. The ultimate source Provider
remains a TaskProvider
of type PackageApplication
, and you'll also have the transformed target PackageApplication.getOutputDirectory()
:
provider = {FlatMapProvider} "flatmap(provider(task 'packageStagingDebug' \
class com.android.build.gradle.tasks.PackageApplication))"
provider = {TaskCreatingProvider_Decorated} \
"provider(task 'packageStagingDebug', \
class com.android.build.tasks.PackageApplication"
transformer = {SingleInitialProviderRequestImpl$On$2} \
SingleInitialProviderRequestImpl$on$2
from = {PackageApplication$CreationAction\
$handleProvider$operationRequest$2} \
fun PackageApplication.getOutputDirectory(): \
org.gradle.api.file.DirectoryProperty!
This structure comes from the variable view at the breakpoint of variant.artifacts.get(SingleArtifact.APK)
, and you can find the source for other Artifacts the same way. But here's the good news: You don't have to dwell on these details in your daily routine. Just remember that the Provider you acquire from the Artifact API will handle dependencies for you automatically. It's quite handy and neat.
Circling back to our Task, we've transitioned the input and output definitions from File in API v1 to XxxProperty
. These Properties will remain dormant until we activate the Task Action. The builtArtifactsLoader
, acting as a utility class, doesn't influence the input or output, so we've marked it with @Internal
for Gradle to identify. And just like that, our nifty task of fetching the APK and renaming it is wrapped up.
3-3-6: Artifact - Create, Transform, Append
Remember the hassle of obtaining the merged AndroidManifest.xml with API v1? With API v2, we get to enjoy a more straightforward approach, one that we might find reminiscent of the process we use for fetching the APK.
val mergedManifestProvider = variant.artifacts
.get(SingleArtifact.MERGED_MANIFEST) // [I]
...
// project.tasks
// .withType(ProcessMultiApkApplicationManifest::class.java)
// .first { it.name.contains(variant.name, true) }
// .dependsOn(postUpdateManifestTask) // [II]
The line marked [I] allows us to snag the Provider
of AndroidManifest.xml, and it automatically takes care of the Task's prepose dependency. But if you recall using API v1 in Section 3-2, we also had to stitch a postpose dependency, as seen in the line marked [II]. Well, for modifying and merging AndroidManifest.xml, Artifact API v2 has paved an easier path for us.
val postUpdateTask = project.tasks
.register<ManifestAfterMergeTask>(
"postUpdate${variantCapitalizedName}Manifest")
variant.artifacts
.use(postUpdateTask) // [I]
.wiredWithFiles( // [II]
ManifestAfterMergeTask::mergedManifest,
ManifestAfterMergeTask::updatedManifest
)
.toTransform(SingleArtifact.MERGED_MANIFEST) // [III]
...
abstract class ManifestAfterMergeTask : DefaultTask() {
@get:InputFile
abstract val mergedManifest: RegularFileProperty
@get:OutputFile
abstract val updatedManifest: RegularFileProperty
@TaskAction
fun afterMerge() {
val modifiedManifest = mergedManifest.get().asFile.readText()
.replace("allowBackup=\"true\"", "allowBackup=\"false\"")
updatedManifest.get().asFile.writeText(modifiedManifest)
}
}
Here's how it works:
- At [I], invoking
.use(postUpdateTask)
sets the stage for the operations at [II] and [III], focusing them on thepostUpdateTask
. - At [II],
wiredWithFiles(...)
takes the task's input and output attributes and automatically callsset(...)
internally to pass values to the pair ofRegularFileProperty
. When registering the Task, we only define the variable name; no extra assignments are needed. The AGP wants to systematize this modification step into a pipeline where one Task's output flows right into the next Task's input. This orderly handoff makes it logical for AGP to control the Task's input and output. - The
RegularFileProvider
object that we get fromvariant.artifacts.get(SingleArtifact.MERGED_MANIFEST)
is the final piece of the puzzle, the culmination of the entire pipeline's efforts. This approach lets us choose between modification or retrieval as needed without fretting over product order or file location.
Visualize it like this: AGP's Task for merging the AndroidManifest.xml results in A; our custom Task takes A as input and yields B as output; and finally, we copy the result (B) to the merged_manifests folder, ending up with C (Figure 3.3.6). This streamlined process lets us concentrate on what really matters, without getting bogged down in the minutiae.
Finally, the code at [III] specifies our operation mode toTransform(...)
and the target type MERGED_MANIFEST
. There are three such operations that share this code template:
variant.artifacts
.use(xxxxTask)
.wiredWithFiles(...) / wiredWithDirectories(...) / wiredWith(...)
.toTransform(...) / toCreate(...) / toAppendTo(...)
However, not all 11 Artifacts support Transform operations, and different operation types also affect the selection of the wiredWith*()
function. How do we know what operation combination we need? The simplest way is to look at the source code:
object APK:
SingleArtifact<Directory>(DIRECTORY),
Transformable,
Replaceable,
ContainsMany
object MERGED_MANIFEST:
SingleArtifact<RegularFile>(FILE, Category.INTERMEDIATES,
"AndroidManifest.xml"),
Replaceable,
Transformable
object ALL_CLASSES_JARS:
MultipleArtifact<RegularFile>(FILE),
Appendable,
Transformable,
Replaceable
In this section, we've pulled out two main artifacts: APK and MERGED_MANIFEST, supplementing them with ALL_CLASSES_JARS
. Let's delve into what this all means:
Artifacts and Their Cardinality: An Artifact can take on one of two forms,
SingleArtifact
orMultipleArtifact
, reflecting the cardinality of its associatedFileSystemLocation
instance. The generic type of either aRegularFile
or aDirectory
outlines the nature of the target artifact. If it's aSingleArtifact
, the final delivery could be either a single file or a folder, whereas aMultipleArtifact
might consist of multiple files or folders. Interestingly, a product folder can house multiple files, a concept embodied byContainsMany
.Detailing the Artifacts: The constructor parameters for
SingleArtifact
andMultipleArtifact
offer more insights into the nature of the artifacts. For instance,Category.INTERMEDIATES
tells us we're dealing with an intermediate product. You'll find five different artifact attributes here:SOURCES
,GENERATED
,OUTPUTS
, andREPORTS
.Interfaces and Operations: The artifacts above implement three particular interfaces,
Appendable
,Transformable
, andReplaceable
, each corresponding to one of the three key operations detailed in the code template below. (Figure 3.3.7)- Appending with
Appendable
: This leaves existing artifacts untouched, and new Tasks simply add more artifacts to the existing list. Currently, this is only supported byMultipleArtifact
. - Transforming with
Transformable
: This lets you modify or transform existing artifacts, and bothSingleArtifact
andMultipleArtifact
are on board with this. - Replacing with
Replaceable
: This option allows you to replace the original artifacts entirely. It's not about modifying them based on input; it's about a complete substitution. This is a feature exclusively forSingleArtifact
.
- Appending with
Now, let's connect the dots between these three operations, the four wiredWith*()
methods, and the *OperationRequest
classes they spawn.
Method(wiredWith*) | wiredWith*() generates corresponding OperationRequest | OperationRequest supports Operation |
---|---|---|
wiredWith(taskOutput: (TaskT) -> FileSystemLocationProperty<FileTypeT>) | OutOperationRequest | toAppendTo(...) / toCreate(...) |
wiredWith(taskInput: (TaskT) -> ListProperty<FileTypeT>, taskOutput: (TaskT) -> FileSystemLocationProperty<FileTypeT>) | CombiningOperationRequest | toTransform(...) |
wiredWithDirectories(taskInput: (TaskT) -> DirectoryProperty, taskOutput: (TaskT) -> DirectoryProperty) | InAndOutDirectoryOperationRequest | toTransform(...) / toTransformMany(...) |
wiredWithFiles(taskInput: (TaskT) -> RegularFileProperty, taskOutput: (TaskT) -> RegularFileProperty) | InAndOutFileOperationRequest | toTransform(...) |
Having walked through the analysis, you're probably feeling right at home with Artifact API v2 by now. To make sure we've got everything nailed down, let's craft a small example that will help cement what we've covered in this section. What about adding an extra file to the assets directory? It'll be packed into the APK.
Here's the Kotlin code that makes it happen:
val addAssetTask = project.tasks.register<AddAssetTask>(
"AddAssetTask${variantCapitalizedName}") {
additionalAsset.set(project.file("app_key.txt"))
}
variant.artifacts.use(addAssetTask)
.wiredWith(AddAssetTask::outputDirectory)
.toAppendTo(MultipleArtifact.ASSETS)
...
abstract class AddAssetTask: DefaultTask() {
@get:InputFile
abstract val additionalAsset: RegularFileProperty
@get:OutputFiles
abstract val outputDirectory: DirectoryProperty
@TaskAction
fun addAsset() {
val target = additionalAsset.get().asFile
val assetDir = outputDirectory.get().asFile
assetDir.mkdirs()
target.copyTo(File(assetDir, target.name))
}
}
3-3-7: Improvements and What Lies Ahead for API v2
So what's new and exciting with Variant/Artifact API v2? Here's the scoop:
- Separation and Isolation: the package is being broken down and an independent
AndroidComponentsExtension
is isolated. This separation allows it to provide APIs for third-party services. - Lifecycle Enhancements: The Variant lifecycle has been rearranged with multiple callback nodes, support for object locking, and specialized configuration handling at each node.
- A Stable and Shiny Artifact API: As of AGP 7.2, developers have access to 11 well-defined artifacts, backed by Gradle's new Lazy Configuration API.
- Explicit and Simplified Artifacts: These artifacts boast clearer semantics and a streamlined process.
But wait, there's still room to grow:
- From version 4.x test to 7.0 official, artifacts jumped from 0 to 7;
- 7.1 brought the count to 10;
- And 7.2 and 7.3 have nudged that up to 11.
As we survey the recent Artifact updates, we spot a trend: the growth in artifacts for each minor or major version is modest. The Artifacts API will likely continue to evolve over the next few versions (see Section 7-3). When the existing APIs fall short, developers might have to fall back on traditional hooking methods, or even hook into other internal components. In the following chapters of this book, we'll dig into the AGP entry configuration process and ponder how to wrap up third-party Artifact API sets.
3-3-8: In Summary
Functions like get
, transform
, append
, and create
in Artifact's world should feel like second nature now. If you're curious about the Artifacts or operation methods we didn't touch on here, why not explore them on your own? As we continue our journey in the chapters ahead, we'll peel back more layers of the Artifact API, uncovering new ways to apply what we've learned.