How to Build a Custom Gradle Plugin
Introduction
This article will explore some basic understanding of Gradle and how to create a custom Gradle plugin.
What is a Gradle file?
Gradle -> Build Automator
- In an Android project, a Gradle Scripts folder contains Gradle files. Here, you can find two build.gradle files.
- The build.gradle (Project) file includes configurations related to the project, such as the version of Compose that will be used, as well as plugins for the application, libraries, and Kotlin.
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
compose_version = '1.0.2'
}
dependencies {
id 'com.android.application' version '7.1.0' apply false
id 'com.android,library' version '7.1.0' apply false
id 'org.jetbrains.kotlin.android' version '1.5.21' apply false
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
2. The build.gradle (Module: app) file contains configurations specific to the app. For example, it includes an Android block specifying versions for the compile SDK, the application ID, and other related settings.
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
compileSdk 32
defaultConfig {
applicationId "com.sample.gradlemigrationsample"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
release {
minifyEnabled false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion '1.5.21'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
// Compose dependencies
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-beta01"
implementation "androidx.navigation:navigation-compose:2.4.0-alpha09"
implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0-alpha03"
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1'
//Dagger - Hilt
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-android-compiler:2.37"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
kapt "androidx.hilt:hilt-compiler:1.0.0"
implementation "androidx.room:room-runtime:2.3.0"
kapt "androidx.room:room-compiler:2.3.0"
implementation "androidx.room:room-ktx:2.3.0"
}
It takes all the product configurations defined in these Gradle files and executes various tasks in the correct order.
- Every Android project includes a gradlew file, where “w” stands for wrapper. This binary executable file contains a script that ensures the specified version of Gradle is used to run certain tasks.
This provides a basic understanding and functionality of Gradle. Now, we can explore how to build a custom Gradle plugin.
Why do we need a Custom Gradle Plugin?
- We can manage the common configurations in a centralized manner, eliminating the need to reapply settings for each individual module.
- This custom plugin will be similar to “libs.plugins.jetbrains.kotlin.android” and other plugins we utilize in build.gradle.
- To begin writing a custom plugin, you can create a class in the buildSrc folder. For example, you might name the class CustomGradlePlugin.kt. This is essentially just a standard Kotlin class.
- Extend the CustomGradlePlugin class by implementing the Plugin<Project> interface, as it needs to be applied within a Gradle project. This will override the “apply()” method, which is called whenever the plugin is applied in code, specifically when it is defined in the Plugins{} block of the build.gradle file.
class CustomGradlePlugin: Plugin<Project> {
override fun apply(project: Project) {
//TODO
}
}
- Now we can create our own apply method within which we can apply our configuration plugins using applyGradlePlugins(project: Project). Inside this method, we can call the plugins defined in the build.gradle plugins{} block, for example, plugin(“kotlin.android”). We can call this in the apply method.
class CustomGradlePlugin: Plugin<Project> {
override fun apply(project: Project) {
applyGradlePlugins(project)
}
private fun applyGradlePlugins(project: Project) {
project.apply {
plugin("android-library")
plugin("kotlin-android")
plugin("kotlin-kapt")
}
}
}
- Now we can set project configurations within the android {} block. To access the Android library extension, we can create an extension function that allows us to utilize the Android SDK.
- We can now use project.android().apply { .. }, giving us access to the android { … } section in build.gradle (Module: app), where we define the compileSdk version.
- To manage all these configuration versions, we can create a separate class called ProjectConfig.
object ProjectConfig {
const val appId = "com.sample.gradlemigrationsample"
const val minSdk = 21
const val compileSdk = 32
const val targetSdk = 32
const val versionCode = 1
const val versionName = "1.0"
}
- Now we can create a new method called setProjectConfiguration(), which allows us to establish the configuration for our project within the android{} block. This includes settings such as defaultConfig, compileSdk, testInstrumentationRunner, and buildTypes.
import org.gradle.api.Project
class CustomGradlePlugin: Plugin<Project> {
override fun apply(project: Project) {
applyGradlePlugins(project)
setProjectConfiguration(project)
}
private fun applyGradlePlugins(project: Project) {
project.apply {
plugin("android-library")
plugin("kotlin-android")
plugin("kotlin-kapt")
}
}
private fun setProjectConfiguration(project: Project) {
project.android().apply {
compileSdk = ProjectConfig.compileSdk
defaultConfig {
minSdk = ProjectConfig.minSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_18
targetCompatibility = JavaVersion.VERSION_18
}
}
}
private fun Project.android(): LibraryExtension {
return extensions.getByType(LibraryExtension::class.java)
}
}
- We no longer need to include android{..} in the code, and we can simply use our custom Gradle plugin by applying <CustomGradlePlugin>().
plugins {
id 'com.android.application'
id 'kotlin-android'
}
apply<CustomGradlePlugin>()
android {
namespace = "com.sample.gradlemigrationsample"
}
dependencies {
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0-beta01"
implementation "androidx.navigation:navigation-compose:2.4.0-alpha09"
implementation "androidx.compose.material:material-icons-extended:$compose_version"
implementation "androidx.hilt:hilt-navigation-compose:1.0.0-alpha03"
}
Here, we can see that we have included the plugin ID ‘com.android.application’ and ‘kotlin-android’, even though it is part of our custom plugin. This is because Gradle evaluates our custom plugin later than the plugin{} block, which reflects the order in which Gradle processes them.
Conclusion
- This approach is very beneficial in multi-module projects where various build types exist. Each module has its build.gradle file, allowing for the use of project configuration files to organize code effectively.
Thanks for reading…