Skip to content
Book Cover of Extending Android Builds

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.

Figure 3.3.1: :gradle and :gradle-api libraries

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)

Figure 3.3.2: com.android.tools.* libraries

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:

Kotlin
// 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.

Kotlin
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:

Kotlin
// build.gradle.kts
androidComponents {
  onVariants { variant ->
    ...
  }
}

Here's a quick comparison of v1 and v2 Extensions:

v1v2
Classcom.android.build.gradle.AppExtensioncom.android.build.api.variant.ApplicationAndroidComponentsExtension
Library:gradle:gradle-api
Extension NameandroidandroidComponents
Traversal MethodapplicationVariants.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:

Kotlin
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:

Kotlin
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:

Figure 3.3.3: New lifecycle for AGP Variants

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 {...}.

Figure 3.3.4: finalizeDsl(...) APIs

Kotlin
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:

Kotlin
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)

Figure 3.3.5: beforeVariants(...) APIs

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:

Kotlin
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.

Kotlin
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 Variants 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:

Kotlin
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:

Kotlin
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)

Artifactv2
SA.AARGets the final AAR file of the Library
SA.APKGets the folder containing the final APK(s) of the Application
SA.BUNDLEGets the final AAB file of the Application
SA.MERGED_MANIFESTGets the merged AndroidManifest.xml file
SA.METADATA_LIBRARY_DEPENDENCIES_REPORTGets the metadata of the Library dependencies
SA.OBFUSCATION_MAPPING_FILEGets the obfuscation mapping.txt
SA.PUBLIC_ANDROID_RESOURCES_LISTGets the publish.xml file of the Library (used to specify required classes in main DEX file)
MA.ALL_CLASSES_DIRSGets source folders of all classes for DEX (removed in AGP 8.0, see Section 7-3)
MA.ALL_CLASSES_JARSGets JAR packages of all classes for DEX (removed in AGP 8.0, see Section 7-3)
MA.ASSETSGets all Assets for APK or Bundle
MA.MULTIDEX_KEEP_PROGUARDGets 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:

Kotlin
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:

Kotlin
fun <FileTypeT: FileSystemLocation> get(
  type: SingleArtifact<FileTypeT>
): Provider<FileTypeT>

Its counterpart for multiple artifacts is:

Kotlin
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:

kotlin
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.

kotlin
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.

kotlin
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 the postUpdateTask.
  • At [II], wiredWithFiles(...) takes the task's input and output attributes and automatically calls set(...) internally to pass values to the pair of RegularFileProperty. 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 from variant.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.

Figure 3.3.6: AndroidManifest.xml at different stages

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:

Kotlin
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:

Kotlin
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:

  1. Artifacts and Their Cardinality: An Artifact can take on one of two forms, SingleArtifact or MultipleArtifact, reflecting the cardinality of its associated FileSystemLocation instance. The generic type of either a RegularFile or a Directory outlines the nature of the target artifact. If it's a SingleArtifact, the final delivery could be either a single file or a folder, whereas a MultipleArtifact might consist of multiple files or folders. Interestingly, a product folder can house multiple files, a concept embodied by ContainsMany.

  2. Detailing the Artifacts: The constructor parameters for SingleArtifact and MultipleArtifact 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, and REPORTS.

  3. Interfaces and Operations: The artifacts above implement three particular interfaces, Appendable, Transformable, and Replaceable, 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 by MultipleArtifact.
    • Transforming with Transformable: This lets you modify or transform existing artifacts, and both SingleArtifact and MultipleArtifact 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 for SingleArtifact.

Figure 3.3.7: Three Artifact operations

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 OperationRequestOperationRequest supports Operation
wiredWith(taskOutput: (TaskT) -> FileSystemLocationProperty<FileTypeT>)OutOperationRequesttoAppendTo(...) / toCreate(...)
wiredWith(taskInput: (TaskT) -> ListProperty<FileTypeT>, taskOutput: (TaskT) -> FileSystemLocationProperty<FileTypeT>)CombiningOperationRequesttoTransform(...)
wiredWithDirectories(taskInput: (TaskT) -> DirectoryProperty, taskOutput: (TaskT) -> DirectoryProperty)InAndOutDirectoryOperationRequesttoTransform(...) / toTransformMany(...)
wiredWithFiles(taskInput: (TaskT) -> RegularFileProperty, taskOutput: (TaskT) -> RegularFileProperty)InAndOutFileOperationRequesttoTransform(...)

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:

Kotlin
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.