2-5: Classification of Gradle Plugins β
In previous sections, we've introduced the concepts of Script Plugins and Binary Plugins (Section 2-1), and figured out the functionality of Pre-compiled Script Plugins (Section 2-2). What else lurks beneath the plugin architecture of Gradle? In this section, we'll categorize Gradle plugins in multiple dimensions and illustrate with simple examples.
The source code for this section is available in the project-structure project.
2-5-1: Categorizing by Implementation Type β
Gradle plugins can be implemented in two ways:
- Script files with the *.gradle(.kts) extension.
- Classes implementing the
org.gradle.api.Plugin
interface.
Let's break them down.
Script Plugin β
Let's give a key concept: in the Gradle lexicon, scripts beyond the standard init.gradle(.kts), build.gradle(.kts) (including those named differently, i.e., app.gradle.kts as the module's build script), and settings.gradle(.kts) are treated as plugins, specifically, Script Plugins. Examples include maven-publish.gradle.kts
and jacoco.gradle
scripts that can be found in some open source projects, indicating their roles in Maven publishing and Jacoco code coverage testing, respectively.
The *.gradle.kts script plugins that adhere to the Gradle Kotlin DSL will be the primary topic of this section:
- independent Script Plugin: These have no specific housing location and can include any stand-alone script (even remote ones) that is accessible by the primary script (such as build.gradle(.kts)). We often see them placed in $rootProjectDir/ or $rootProjectDir/gradle/ directories.
- Pre-compiled Script Plugin: These scripts reside in certain modules that get compiled before the major project build scripts, and their compiled output is added to the build classpaths automatically. They can be further classified into two types based on their storage location and method of introduction: independent modules (incorperated by Composite Build) and buildSrc module.
Now, consider a scenario that we want to configure Lint parameters uniformly for all Android Library modules in the same project. The following code snippets illustrate how the two types of scripts implement this logic:
βββ project-structure
β βββ app
β βββ build.gradle.kts
β βββ buildSrc
β βββ src
β βββ main
β βββ kotlin
β βββ me.xx2bab.extendagp.buildsrc
β βββ lib-convention-script-plugin.gradle.kts [I]
β βββ standalone-scripts
β βββ lib-convention-script-plugin2.gradle.kts [II]
Firstly, we look at the Pre-compiled Script Plugin at [I]. This script plugin in buildSrc extends the lint-related configuration already presented in Section 2-1 to meet our requirements:
// buildSrc/src/main/kotlin/.../lib-convention-script-plugin.gradle.kts
plugins {
id("com.android.application")
}
android {
lint {
abortOnError = false
}
}
println("The lib-convention-script-plugin" +
"from ./buildSrc is applied.")
The Lint configuration above appears no different from the one in the build.gradle.kts of an Android Library.
// buildSrc/build.gradle.kts
plugins {
`kotlin-dsl` [III]
...
}
repositories {
google()
...
}
dependencies {
implementation("com.android.tools.build:gradle:7.2.2") [IV]
...
}
By the end of Section 2-1, we didn't fully explain how to convert a Pre-compiled Script Plugin into a Plugin that external scripts can load. The magic lies in the kotlin-dsl
plugin at mark [III], it pulls in a suite of plugins for the Kotlin Compiler, the java-gradle-plugin and the Gradle precompiled-script-plugin, encapsulated above plugins in the main apply(...)
method. Furthermore, it includes some Kotlin-related runtime dependencies. Consequently, the script is compiled into a regular Binary Plugin.
For the Precompiled Script Plugin to reference and configure the AGP, we also need to add relevant dependencies in the build.gradle.kts of buildSrc (see code at point [IV]).
How does an Independent Script Plugin achieve the same result? Let's take a look:
// standalone-scripts/lib-convention-script-plugin2.gradle.kts
import com.android.build.gradle.LibraryExtension
buildscript {
repositories {
mavenCentral()
google()
gradlePluginPortal()
}
dependencies {
classpath("com.android.tools.build:gradle:7.2.2")
...
}
}
apply(plugin = "com.android.library")
configure<LibraryExtension> {
lint {
abortOnError = false
}
}
println("The lib-convention-script-plugin2"
+ "from ./standalone-scripts is applied.")
As an independent script, unlike buildSrc that has a complete project structure, this script faces several limitations of Gradle Kotlin DSL:
- It must declare any dependencies (including plugins) required by the script at its outset using
buildscript {}
. - It cannot use the
plugins { id(...) }
form for plugin introduction, and instead useapply(plugin = "...")
. - Due to the fact that
apply(plugin = "...")
does not support Type-Safe Model Accessors, it cannot directly utilize theandroid {...}
configuration entry, and has to rely on the raw Gradle APIproject.configure<T>(...)
.
Again, as we mentioned in Section 2-3, these constraints are non-existent in Gradle Groovy DSL, and people are accustomed to using Independent Script Plugins directly. However, within the Gradle Kotlin DSL context, Precompiled Script Plugin is the recommended method, providing the advantages of unified management and Type-Safe Model Accessors. Below, we summarize the primary differences between the two plugin types:
Independent Script Plugin | Precompiled Script Plugin | |
---|---|---|
Specified directory | None | Either buildSrc or another independent JVM project that usually references the kotlin-dsl plugin |
Applying script plugins to build scripts | Limited to apply(from = "...") , plugins{ id(...) } mode unsupported | Supports both plugins { `plugin-id` } and apply(from = "...") |
Applying plugins in script plugin | Limited to apply(from = "...") , plugins{ id(...) } mode unsupported | Supports both plugins { `plugin-id` } and apply(from = "...") |
External dependency references | Independently declared using buildscript(...) | Dependencies declared based on the project's own build.gradle.kts |
Type-Safe Model Accessors | Unsupported | Supported |
Binary Plugin β
Binary Plugins are a common aspect of Gradle Plugin development, typically manifesting as XxxPlugin
classes in Kotlin, Groovy or Java. These classes implement the org.gradle.api.Plugin
interface. Notably, a Binary Plugin can be explicitly declared within a Script Plugin as well:
// buildSrc/src/main/kotlin/.../dummy-script-plugin.gradle.kts
class DummyBinaryPluginInScript: Plugin<Project> {
// Avoid using "project" as the parameter name due to
// a scope conflict with the script's `project` variable.
override fun apply(target: Project) {
println("The DummyBinaryPluginInScript from"
+ "./buildSrc is applied with ${target.name}.")
}
}
apply<DummyBinaryPluginInScript>()
In this context, Plugins are referenced exclusively through the apply(...)
method. To adopt the function of the Precompiled Script Plugin, classes must reside in a separate module with full project structure and the Gradle build script, which is then added to the build classpaths dependencies. Depending on project locations and incorporation methods, these can be divided into two types: independent project modules and buildSrc modules.
Consider the following simple binary plugin that also incorporates the lint configuration function:
// buildSrc/src/main/kotlin/.../LibConventionBinaryPlugin
class LibConventionBinaryPlugin : Plugin<Project> {
override fun apply(target: Project) {
println("The lib-convention-binary-plugin"
+ "from ./buildSrc is applied.")
val android = target.extensions.getByType<LibraryExtension>()
android.lint.abortOnError = false
}
}
The build.gradle.kts in buildSrc is as follows:
// buildSrc/build.gradle.kts
gradlePlugin {
plugins.register("lib-convention-binary-plugin") {
id = "lib-convention-binary-plugin"
implementationClass
= "me.xx2bab.extendagp.buildsrc.LibConventionBinaryPlugin"
}
...
}
The gradlePlugin(...)
configuration serves to register a series of binary plugins to the output jar package, located in build/resources/main/META-INF/gradle-plugins/lib-convention-binary-plugin.properties. This technique follows the Service Provider Interface (SPI) paradigm. Post-module loading, Gradle searches files within all META-INF/gradle-plugins locations, registering compliant plugins to the current available plugin list. It is akin to Google's AutoService tool, which generates resource description files through plugin descriptions or annotations, circumventing the necessity of manual file creation.
// lib-convention-binary-plugin.properties
implementation-class=me.xx2bab.extendagp.buildsrc.LibConventionBinaryPlugin
In most scenarios, it is advantageous to opt for the Binary Plugin in the form of Convention Plugin over the Script Plugin. This approach fosters plugin module segmentation, dependency decoupling, and help organizes intricate plugin logic.
Function Exporting β
We've presented some configuration features implemented by different plugins. However, they are directly applied to the external build.gradle.kts script as a whole, without further interaction. You may question whether we can enable external build.gradle.kts to call specific plugin functions. Alternatively, the Function Export.
For developers who are acquainted with JavaScript, this concept resembles the export
statement from ECMAScript or CommonJS, which discloses JavaScript modules functions, objects, or primitive values to other programs, enabling import via the import
statement. How can we achieve the same goal in Gradle for all types of plugins?
Below is an example to share a script that exports a set of useful utilities by retrieving all the Build Feature switches for the currently loaded Android Library module and producing a Map
. Firstly, we concentrate on Pre-compiled Plugins and simply define a regular Kotlin file to contain the function:
// buildSrc/src/main/kotlin/.../AppBuildFeatureExport.kt
fun getAppFeatureSwitchesFromDotKt(
android: ApplicationExtension): Map<String, Boolean?> {
return mapOf(
"dataBinding" to android.buildFeatures.dataBinding,
"mlModelBinding" to android.buildFeatures.mlModelBinding,
"prefab" to android.buildFeatures.prefab,
"aidl" to android.buildFeatures.aidl,
"buildConfig" to android.buildFeatures.buildConfig,
"compose" to android.buildFeatures.compose,
"renderScript" to android.buildFeatures.renderScript,
"shaders" to android.buildFeatures.shaders,
"resValues" to android.buildFeatures.resValues,
"viewBinding" to android.buildFeatures.viewBinding
)
}
// app/app.gradle.kts
import me.xx2bab.extendagp.buildsrc.getAppFeatureSwitchesFromDotKt
afterEvaluate {
println(getAppFeatureSwitchesFromDotKt(android))
}
.kt
files lack awareness of the context for the current host script, such as build.gradle.kts
being hosted in the execution environment of an org.gradle.api.Project
instance as context, the android: ApplicationExtension
extension we require is dependent on an external input. Nevertheless, for the same workspace, adding feature switches from Gradle would require yet another parameter gradle: Gradle
, further complicating parameter management. Given the limited scalability of the first solution, we propose an optimized second solution: passing the Project
instance instead or leveraging the Kotlin Function Extension feature.
// buildSrc/src/main/kotlin/.../AppBuildFeatureExport.kt
fun Project.getAppFeatureSwitchesFromDotKt2(): Map<String, Boolean?> {
val android = extensions.getByType<ApplicationExtension>()
return mapOf(
"dataBinding" to android.buildFeatures.dataBinding,
...
"viewBinding" to android.buildFeatures.viewBinding,
"caching" to findProperty("org.gradle.caching").toString().toBoolean(),
"parallel" to findProperty("org.gradle.parallel").toString().toBoolean()
)
}
// app/app.gradle.kts
import me.xx2bab.extendagp.buildsrc.getAppFeatureSwitchesFromDotKt2
afterEvaluate {
println(getAppFeatureSwitchesFromDotKt2())
}
In above code snippet, we transformed the ordinary function into an extension function. This modification allows us to create a call-context mirroring the host script without depending on external parameters. The data required can be obtained internally via the Project
, such as extensions.getByType<ApplicationExtension>()
.
When an Independent Script Plugin takes over the job, the above solution becomes inapplicable. In such cases, the following third solution may be employed:
// standalone-scripts/app-build-features-export2.gradle.kts
extra["getAppFeatureSwitches"]
= fun(): Map<String, Boolean?> {
val android = extensions.getByType<ApplicationExtension>()
return mapOf(
"dataBinding" to android.buildFeatures.dataBinding,
...
"viewBinding" to android.buildFeatures.viewBinding,
"caching" to project.findProperty("org.gradle.caching")
.toString().toBoolean(),
"parallel" to project.findProperty("org.gradle.parallel")
.toString().toBoolean()
)
}
// app/app.gradle.kts
afterEvaluate {
val funcFromScript = extra["getAppFeatureSwitches"] as () -> Map<...>
println(funcFromScript())
}
As mentioned in Section 2-4, Project
, Setting
, and Gradle
implement the ExtensionAware
interface, which facilitates access to an ExtraPropertiesExtension
extension through the extra
property for Gradle Kotlin DSL or ext
for Gradle Groovy DSL. This extension is designed for appending custom domain objects, which has the API style and internal structure that resembles Map
. The above script first integrates a function named getAppFeatureSwitchesFromScriptPlugin
into extra
, and subsequently retrieves it for execution in app.gradle.kts
.
Although Independent Script Plugins in Gradle Kotlin DSL do not possess the complete context of the host script, necessitating the explicit declaration and importation of any external dependencies, they remain Gradle scripts capable of accessing the raw API of the Gradle platform and utilizing some of its "bridges" to share data.
2-5-2: Categorizing by Function Scopes β
In another dimension, Gradle plugins can be categorized functionally into two groups:
- Convention Plugin: Primarily focuses on the Configuration phase with configuration convention across multiple Projects.
- Domain Specific Plugin: Primarily focuses on specific domain functions with a set of Tasks.
For additional details regarding the three phases of the Gradle lifecycle, please refer to Sections 2-2 and 4-1.
Convention Plugin β
The term "convention" in this context refers to the default configuration. The Convention Plugin provides reusable logic mainly for the Gradle Configuration phase by standardizing certain configurations across modules of varying traits,Β such as setting up SDK/Java/Kotlin versions in all library modules and dynamic-features modules.
Both lib-convention-script-plugin.gradle.kts and LibConventionBinaryPlugin
from the preceding section are typical examples of Convention Plugins, applying identical Lint configuration conventions to multiple Android Library modules that reference them.
While Convention Plugins are usually manifested as Script Plugins, Binary Plugins can be implemented as well, particularly due to the greater convenience of writing and reading Gradle configuration in declarative DSLs, thereby reducing some boilerplates.
You can achieve a similar effect by using APIs such as subprojects
and allprojects
in the parent-level build.gradle.kts (typically located in the project root) to configure all modules at one place, namely "Cross Project Configuration". However, compared to the Convention Plugin, there are several notable disadvantages:
- Increased coupling between the sub-project and the parent project (root build.gradle.kts ) may lead to the failure of some caching or on-demand configuration mechanisms.
- The parent project's changes to the sub-project configuration are not explicit. Without prior knowledge, changes made by the parent project to the sub-project's build logic can be more of a hassle when troubleshooting.
- If you wish not to apply the changes to all submodules (for instance, if you want to configure the Android Library and skip the Android Application), you'll need to include conditional statements such as
if...else...
, which is not ideal for module customization in the long term.
Consequently, we advise using the Convention Plugin for abstracting multi-modules reusable configurations.
Domain-Specific Plugin β
On the other hand, the Domain-Specific Plugin places more emphasis on the execution phase by introducing new Tasks, Task Actions, and so forth for requirements of specific domain. This classification and naming convention is specific to this book since there is no analogous categorization in the Gradle documentation. The goal is to identify the plugin type that is opposite to the Convention Plugin from a functional perspective.
Furthermore, the Domain Specific Plugins can be divided into three sub-categories: Gradle Ext Plugin, Ecosystem Plugin, and Ecosystem Synergy Plugin. Again, these sub-categories are used within this book only. The overall connection is depicted in the following Figure 2.5.1:
The left part of the diagram are the base platform (Gradle) and ecosystems. Those Plugins (right part) dependent on them are higher-level Plugins.
Gradle Ext Plugin: As Gradle is often considered a platform providing basic capabilities like defining build stages, Task mechanisms, dependency management, and so forth, a Plugin that depends solely on Gradle and enhances the above basic functions is deemed a Gradle Ext Plugin.
// buildSrc/src/main/kotlin/.../BasisExtPlugin
class BasisExtPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.afterEvaluate {
target.configurations.first { cf ->
cf.name == "implementation"
}.let { cf ->
println("${cf.name} = ${cf.dependencies.size}")
cf.dependencies.forEach { println(it.toString()) }
}
}
}
}
This Plugin collects and displays the set of dependencies of the implementation
configuration. It only depends on the configuration features of Gradle and can be utilized across various language and ecosystem builds. Here are some common open source examples:
- gradle-dependency-analyze/gradle-dependency-analyze(https://github.com/gradle-dependency-analyze/gradle-dependency-analyzen): A tool for detecting if dependencies are in use.
- cashapp/licensee(https://github.com/cashapp/licensee): A tool for verifying if dependencies fall within the specified License range of the project.
- maven-publish(https://docs.gradle.org/current/userguide/publishing_maven.html): The official Gradle Plugin for publishing build products to MavenCentral.
Ecosystem Plugin: Builds upon Gradle to provide ecosystem-specific features, typically referring to Plugins related to a language (Java, Kotlin) or frameworks (Spring, Android). Here are some common open-source examples:
- Android Gradle Plugin(https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/README.md): A Plugin set that facilitates various Android build processes.
- kotlin-android(https://github.com/JetBrains/kotlin/tree/master/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/android): The Plugin is created to integrate Kotlin into Android development. The Kotlin compilations are configurated to be part of the builds of Android variatns.
Ecosystem Synergy Plugin: Further extensions based on both Gradle and Ecosystem Plugins. The majority of Plugins presented in this book are Synergy Plugins for the Android Ecosystem. Nevertheless, if a plugin does not need to be heavily dependent on AGP but only relies on its final compilation result (e.g. APK/AAB), it can be crafted into a Gradle Ext Plugin to expand its scope of application. A typical Android Ecosystem Synergy Plugin sample shows the following:
// buildSrc/src/main/kotlin/.../EcoCoordinatorPlugin
class EcoCoordinatorPlugin: Plugin<Project> {
override fun apply(p: Project) {
val android = p.extensions.getByType<AppExtension>()
android.applicationVariants.configureEach {
val variant = this
val checkResourceTask = p.tasks.register(
"check${variant.name.capitalize()}ResourceTask") {
doLast {
println(variant.mergeResourcesProvider.get()
.outputDir.asFile.get().absolutePath)
}
this.dependsOn(variant.mergeResourcesProvider)
}
variant.assembleProvider.configure {
dependsOn(checkResourceTask)
}
}
}
}
The plugin above retrieves the output directory of the AGP MergeResource
Task. Several open-source examples include:
- Triple-T/gradle-play-publisher(https://github.com/Triple-T/gradle-play-publisher): A Gradle plugin automating the publishing of APK/AAB files to the Play Store.
- 2BAB/Seal(https://github.com/2BAB/Seal): A Gradle Plugin designed to resolve conflicts in AndroidManifest.xml merges.
- App Versioning(https://github.com/ReactiveCircus/app-versioning): A tool for dynamically setting versionCode and versionName values based on Git Tags, leveraging the version-related fields of the AGP Variant.
2-5-3: In Summary β
In this section, we have classified Gradle Plugins into two dimensions: implementation Type and Function Scope. To assist with memory retention, we've also created the following mind map (Figure 2.5.2).