How to Build a Custom Gradle Plugin

Richa Sharma
5 min readAug 19, 2024

--

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.
  1. 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…

--

--

No responses yet