Gradle Plugin Tutorial for Android: Getting Started

Learn how to create a Gradle plugin within your existing Android app, or as a standalone project that you can publish and use in any Android project. By Bhavesh Misri.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Creating a Plugin Using the buildSrc Directory

Now you’ll create a plugin in a different way. First, create a directory, buildSrc, at the project’s root. This is where you’ll add your build logic, and as it supports Kotlin DSL, you can write the plugin in Kotlin.

Note: If you’re unfamiliar with the concept of buildSrc and how to use it, check out the Gradle Tips and Tricks for Android tutorial.

Inside the new directory, create a file, build.gradle.kts. Inside the file, add the following code:

plugins {
  `kotlin-dsl`
}

repositories {
  mavenCentral()
}

So, what’s happening here? Well, first of all buildSrc is a directory that Gradle looks at while compiling. If it finds any custom build code, it adds that to Gradle’s classpath.

In this file, you’re applying the kotlin-dsl plugin, but if you try to sync the project only by adding this, it won’t work. You need to add mavenCentral() in repositories as well, because the plugin is hosted there. After you’ve added these lines, sync the project and you’ll see a few more directories in the buildSrc folder. Your project structure will look something like this:

Project structure

This means your build.gradle.kts file was successfully added to Gradle’s classpath. Next, right-click on the buildSrc folder and select NewDirectory, and then select src/main/java.

Here, you can start writing your plugins in Kotlin and all the modules can access the plugin. Open the java folder and create a Kotlin class, BuildManager, that implements Plugin:

import org.gradle.api.Plugin
import org.gradle.api.Project

class BuildManager : Plugin<Project> {
  override fun apply(project: Project) {
  }
}

You can create tasks within this apply function in a way similar to how you did in your module level build.gradle file earlier. For this, you’ll create a task which will do a bit more than printing a sentence.

Inside apply, include the following code:

project.task("renameApk") {
 doLast {
   val apkPath = "/outputs/apk/release/app-release-unsigned.apk"
   val renamedApkPath = "/outputs/apk/release/RenamedAPK.apk"
   val previousApkPath = "${project.buildDir.absoluteFile}$apkPath"
   val newPath = File(previousApkPath)

   if (newPath.exists()) {
     val newApkName = "${project.buildDir.absoluteFile}$renamedApkPath"
       newPath.renameTo(File(newApkName))
   } else {
     println("Path does not exist!")
   }
 }
}.dependsOn("build")

Add an import for the File class at the top of the file:

import java.io.File

You’re creating a task called renameApk, and in that class, you’re finding where your APK is located and then renaming it. But what if the APK hasn’t yet been created? Or, what if you removed the file for some reason? Well, that’s where the last line, .dependsOn("build"), comes to the rescue. This function will create a dependency on the build task that will create the APK.

But if you try to execute the task from the terminal, it’ll fail and give you an error saying BUILD FAILED. This is because you havn’t yet applied the plugin to your project. To do that, go into your build.gradle.kts file and add the following:

gradlePlugin {
  plugins {
    create("BuildManager") {
      id = "com.raywenderlich.plugin"
      implementationClass = "BuildManager"
      version = "1.0.0"
    }
  }
}

In the code above, you’re registering your plugin by using one of the extension functions, gradlePlugin. By using the create function, you can give a name, ID, version, and reference of the plugin class you created.

Now it’s time to add the registered plugin to your module. Jump into your module-level build.gradle file and add it as the last item in the plugins task:

plugins {
  ...
  id 'com.raywenderlich.plugin'
}

You’re ready to go! Sync the project, run ./gradlew clean to clean up the build directory and then execute your task with ./gradlew -q renameApk. Go to the /app/build/outputs/apk/release folder and find your renamed APK.

Dependency Management

Before adding more features to your plugin, you’ll clean up the project a bit. Create a new Kotlin file in your java folder and name it Constant. Add the following code in the file:

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

object Dependencies {
  const val kotlinCore = "androidx.core:core-ktx:${Versions.kotlinCoreVersion}"
  const val appCompat = "androidx.appcompat:appcompat:${Versions.appCompatVersion}"
  const val material = "com.google.android.material:material:${Versions.materialVersion}"
  const val constraint = "androidx.constraintlayout:constraintlayout:${Versions.constraintVersion}"
  const val jUnit = "junit:junit:${Versions.jUnitVersion}"
  const val jUnitExt = "androidx.test.ext:junit:${Versions.jUnitExtVersion}"
  const val espresso = "androidx.test.espresso:espresso-core:${Versions.espressoVersion}"

  const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlinStdLibVersion}"
  const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlinStdLibVersion}"
  const val buildToolGradle = "com.android.tools.build:gradle:${Versions.buildToolGradleVersion}"
}

object Versions {
  const val compileSdkVersion = 30
  const val buildToolsVersion = "30.0.3"
  const val minSdkVersion = 21
  const val targetSdkVersion = 30

  const val kotlinStdLibVersion = "1.4.32"
  const val buildToolGradleVersion = "4.1.3"

  const val kotlinCoreVersion = "1.3.2"
  const val appCompatVersion = "1.2.0"
  const val materialVersion = "1.3.0"
  const val constraintVersion = "2.0.4"
  const val jUnitVersion = "4.13.2"
  const val jUnitExtVersion = "1.1.2"
  const val espressoVersion = "3.3.0"
}

object AppDetail {
  const val applicationId = "com.raywenderlich.projecttracker"
  const val appName = "ProjectTracker"
  const val versionCode = 1
  const val versionName = "1.0.0"

  // Change these values as needed
  const val previousPath = "/outputs/apk/release"
  const val targetPath = "newOutput"

  const val previousName = "app-release-unsigned.apk"
  val newApkName = "$appName-${getDate(false)}($versionCode).apk"

  const val dependencyFileName = "Dependencies.txt"
}


fun getDate(forTxtFile: Boolean): String {
  val current = LocalDateTime.now()
  val formatter = if (forTxtFile) {
    DateTimeFormatter.ofPattern("dd MMM, yyy")
  } else {
    DateTimeFormatter.ofPattern("yyyyMMdd")
  }
  return current.format(formatter)
}

The Dependencies, Version and AppDetail classes will be used to manage dependencies, and you’ll also be using the AppDetail class and getDate() function to add more functionality to your plugin. All these do at the moment is to hold the build information in classes. Next, you can jump into the Gradle files of your project and update your dependencies to make it look cleaner and to manage them better.

After the changes, your module-level build.gradle file will look like this:

plugins {
  id 'com.android.application'
  id 'kotlin-android'
  id 'com.raywenderlich.plugin'
}

android {
  compileSdkVersion Versions.compileSdkVersion
  buildToolsVersion Versions.buildToolsVersion

  defaultConfig {
    applicationId AppDetail.applicationId
    minSdkVersion Versions.minSdkVersion
    targetSdkVersion Versions.targetSdkVersion
    versionCode AppDetail.versionCode
    versionName AppDetail.versionName

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  }

  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
  kotlinOptions {
    jvmTarget = '1.8'
  }
}

dependencies {
  implementation Dependencies.kotlinStdLib
  implementation Dependencies.kotlinCore
  implementation Dependencies.appCompat
  implementation Dependencies.material
  implementation Dependencies.constraint
  testImplementation Dependencies.jUnit
  androidTestImplementation Dependencies.jUnitExt
  androidTestImplementation Dependencies.espresso
}

And your top-level or project-level build.gradle file will look like this:

buildscript {
  repositories {
    google()
    mavenCentral()
  }
  dependencies {
    classpath Dependencies.buildToolGradle
    classpath Dependencies.kotlinGradlePlugin
  }
}

allprojects {
  repositories {
    google()
    mavenCentral()
  }
}

task clean(type: Delete) {
  delete rootProject.buildDir
}

With that, it’s time to add more tasks to the plugin class.