3-1: Variant β
Recall back in Section 2-4 when we crafted our first fleshed-out Plugin? That's where we got our first taste of the Variant APIs. But before we dive into how these APIs can shape our custom Plugins, we need to unpack the concept of a Variant itself.
So, what are Variants? Think of them as distinct offspring born from a single project. For instance, a free and a paid version of an app, or multiple iterations of a map SDK drawing from different data sources. Each of these examples represents a Variant. They share the same core code, but special requirements often drive unique configurations for each product, which may include additional code and resources. Within the context of the Gradle platform and the Android/JVM build ecosystem, we'll be focusing on two key Variants: the Variant of Gradle Module Metadata and the Variant of AGP.
The source code relevant to this section can be found in the variant project.
3-1-1: The Variant of Gradle Module Metadata β
Gradle Module Metadata, a type of dependency description file, first appeared on the scene with Gradle 5.3 in 2019. Its purpose is to fill the gaps left by pom.xml or Ivy files. It comes with a *.module file extension and uses JSON as its data format. When published to the common central repository for JVM projects, it still leans on maven-publish
or ivy-publish
for support. Using maven-publish
as an example, you'll see that the *.module file acts as an add-on, getting published to the Maven repository alongside the *.pom file.
βββ .m2
β βββ repository
β βββ me/2bab/extendagp
β βββ library1
β βββ 1.0.1
β βββ ...
β βββ library1-1.0.1.module
β βββ library1-1.0.1.pom
Internal description as follows:
{
"formatVersion": "1.1",
"component": {
"group": "me.2bab.extendagp",
"module": "library1",
"version": "1.0.1",
"attributes": {
"org.gradle.status": "release"
}
},
"createdBy": {
"gradle": {
"version": "7.3.3"
}
},
"variants": [
{
"name": "apiElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.environment": "standard-jvm",
"org.gradle.jvm.version": 11,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-api",
"org.jetbrains.kotlin.platform.type": "jvm"
},
"files": [
{
"name": "library1-1.0.1.jar",
"url": "library1-1.0.1.jar",
"size": 2202,
...
}
]
},
...
]
}
This chapter isn't so much about the file format as it is about the variants
defined within the JSON Object. The particular structure allows us to distinguish between products with the same coordinates, enabling the creation of Variants. Now, imagine a scenario where the Library1 SDK needs to offer a version without an HTTP Client and another with a default HTTP Clientβgiving users the choice.
Following the traditional Maven POM strategy, we could split the core module (which is required) and the Http Client module (which is optional) into separate GAV (Group, Artifact, and Version) coordinates, letting the user decide whether to include the extra Http Client module or not.
But the Variant scheme offered by Gradle Module Metadata has a different approach: it allows the HTTP Client and the core code to be housed in the same module, making it available as an optional capability to external callers.
dependencies {
// Local dependency
implementation(project(":library1")) {
capabilities {
requireCapability("me.2bab.extendagp:library1-okhttp")
}
}
// Or remote dependency
implementation("me.2bab.extendagp:library1:1.0.1") {
capabilities {
requireCapability("me.2bab.extendagp:library1-okhttp")
}
}
}
As shown above, when displaying local or remote dependencies, an additional Capability requirement has been added, which indicates that I need the okhttp
variant of Library1. How can you configure the module to isolate two code parts while publishing to a single coordinate? This practice is aligned with the standard implementation provided by the official Java Plugin of Gradle, and Kotlin can accomplish the same.
plugins {
kotlin("jvm")
`maven-publish`
}
group = "me.2bab.extendagp"
version = "1.0.1"
val SourceSet.kotlin: SourceDirectorySet // [I]
get() = project.extensions
.getByType<KotlinJvmProjectExtension>()
.sourceSets
.getByName(name)
.kotlin
val okhttp = sourceSets.create("okhttp") { // [II]
java.srcDirs("src/okhttp/java", "src/main/java")
kotlin.srcDirs("src/okhttp/kotlin", "src/main/kotlin")
}
java { // [III]
registerFeature("okhttp") {
usingSourceSet(okhttp)
}
}
configurations.named("okhttpImplementation") // [IV]
.get()
.extendsFrom(configurations.implementation.get())
dependencies {
implementation(project(":library1-api"))
implementation(deps.kotlin.std)
"okhttpImplementation"(deps.okHttp) // [V]
}
publishing { // [VI]
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}
Here's the explanation:
- [I] and [II] add new SourceSets to the module, isolating the code with the built-in HTTP client
okhttp
. - [III] configures the Java Plugin Extension, an implementation of Gradle Module Metadata with its Variant. The
registerFeature(...)
method registers the "feature" withokhttp
as the key, enabling the isolation of the independent build environment. After a Gradle Sync, new Tasks likeokhttpClasses
andokhttpJar
will be visible. - Along with this, we have a new Dependency Configuration,
okhttpImplementation
. Through [IV], it inherits all dependencies from theimplementation
, so theokhttp
variant doesn't need to redeclare basic dependencies but only those specific todeps.okHttp
. This is referred to as Variant-Aware Dependency Management. - [VI] is about the configuration of
maven-publish
. Since the Java Plugin manages the source code and dependencies for publishing, we simply configure a standard template here.
The final project structure looks like this:
βββ src
β βββ main
β βββ kotlin
β βββ me/2bab/extendagp.library1
β βββ LibraryApi
β βββ okhttp
β βββ kotlin
β βββ me/2bab/extendagp.library1
β βββ OkHttpClient
βββ library1.gradle.kts
To publish the full structure, use ./gradlew library1:publishMavenPublicationToMavenLocal
(the library-api module that library1 depends on must also be published in the same way):
βββ .m2
β βββ repository
β βββ me/2bab/extendagp
β βββ library1
β βββ 1.0.1
β βββ library1-1.0.1.jar
β βββ library1-okhttp-1.0.1.jar
β βββ library1-1.0.1.module
β βββ library1-1.0.1.pom
Lastly, keep in mind that Gradle Module Metadata is still under development. The latest version 1.1 is not yet widely adopted, but a basic understanding is provided here for comparisons with future AGP variants.
3-1-2: The Variant of AGP β
The Android Gradle Plugin (AGP) works to expand Gradle's capabilities. Even though the AGP Variants have the same requirements, and the basic configurations like SourceSet
and Dependency Configuration can work out-of-the-box, the core differences lie in the nuanced build configurations of Application and Library. These include variations in applicationId
and resValue
, leading to the adoption of a unique variant system.
AGP Variants are statically described in their DSL, and they don't offer direct editing options. Instead, they are constructed from two meta-dimensions: buildTypes
and productFlavors
.
Here's an example code snippet:
defaultConfig {...}
buildTypes {
getByName("debug") {
isMinifyEnabled = false
}
getByName("release") {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile(ββ¦β)
)
}
// create("...") { ... }
}
flavorDimensions += "server"
productFlavors {
create("staging") {
dimension = "server"
applicationIdSuffix = ".staging"
versionNameSuffix = "-staging"
}
create("production") {
dimension = βserver"
applicationIdSuffix = ".production"
versionNameSuffix = "-production"
versionCode = 2
}
}
buildTypes
focuses on development perspectives, while productFlavors
extends from the product viewpoint. If you visualize buildTypes
as the horizontal baseline, then productFlavors
act as the vertical axis, orthogonal to buildTypes
. Together, they create four variants:
debug | release | |
---|---|---|
staging | StagingDebug | StagingRelease |
production | ProductionDebug | ProductionRelease |
You can use specific Gradle commands to output single or multiple APKs. For example, ./gradlew assembleStagingDebug
will output a single variant APK. ./gradlew assemble
can create four different products at once:
βββ app
β βββ outputs
β βββ apk
β βββ production
β βββ debug
β βββ app-production-debug.apk
β βββ release
β βββ app-production-release.apk
β βββ staging
β βββ debug
β βββ app-staging-debug.apk
β βββ release
β βββ app-staging-release.apk
The final products of AGP, such as .apk and .aab files, are typically stored in the outputs folder, while intermediate products like resource.arsc files reside in the intermediates folder. They can all be referred to as Artifacts, a term you'll encounter in subsequent sections.
A quick note on the conventions of these configurations:
- The default
debug
andrelease
ofbuildTypes
are added by AGP. To create additional types, you would use thecreate(...)
orregister(...)
API. - The
defaultConfig {...}
is the baseline forproductFlavors
, with an additionalflavorDimensions
configuration. The overriding sequence between them isproductFlavors
->buildTypes
->defaultConfig {...}
.
It's important to remember that combining multiple flavorDimensions
within productFlavors
can increase the complexity of variant's Cartesian product, potentially impacting performance at both the Configuration and Execution stages.
3-1-3: Android Variant API and Variant-Aware Task β
For Android developers who are using more AGP Variants, understanding different configurations through buildTypes
and productFlavors
and how they differentiate into different variants is essential. But what happens in the subsequent building process?
Assuming you wish to "add an extra Task only for release variant". Many newcomers, including myself when first learning Gradle and AGP, might attempt the following code:
if (gradle.startParameter.taskNames.toString()
.contains("release", ignoreCase = true)) {
tasks.create("runOnReleaseVariantOnly") {
doFirst {
println("Task runOnReleaseVariantOnly: running...")
}
}
}
This approach aims to add a Task if the startParameter
contains the release
keyword without case-sensitive. However, this is a common mistake since it's challenging to anticipate the actual input, and this method doesn't always work. For instance, it won't correctly insert a custom Task with the commonly used assemble
command: it produces four artifacts, including release
, but it does not encompass the release
keyword in the command.
The precise way to handle this is to utilize Variant-Aware Tasks.
androidExtension.applicationVariants.configureEach {
if (this.name.contains("release", ignoreCase = true)) {
tasks.create("runOnReleaseVariantOnly") {
doFirst {
println("runOnReleaseVariantOnly: running...")
}
}
}
}
In AGP, Tasks that indirectly or directly depend on variant properties are referred to as Variant-Aware Tasks. They can be distinguished from normal Gradle tasks that lack this awareness.
Apart from common configurations for Application and Library modules, you should be aware that a set of Variant API configurations has been added when publishing AAR from AGP 7+ for Library modules specifically. Here's an illustrative example:
android {
publishing {
multipleVariants("custom") {
includeBuildTypeValues("debug", "release")
includeFlavorDimensionAndValues(
dimension = "color",
values = arrayOf("blue", "pink")
)
includeFlavorDimensionAndValues(
dimension = "shape",
values = arrayOf("square")
)
}
}
}
The AGP Variant API has two versions: v1 (still available in 7.x but marked as deprecated) and v2 (incubated since 4.1 and officially released in 7.0). We will see more information in the next few sections.
3-1-4: In Summary β
Android Variants, composed of BuildTypes and ProductFlavors, provide a powerful and flexible way to manage different build scenarios, and Variant-Aware Tasks play an essential role in this process. (Figure 3.1.1)
Overall, the configuration and utilization of Android variants provide a powerful framework for customizing the build process. Both Plugin developers and users should learn these concepts to enable precise customization and effective use of AGP.