Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
218: Writing a Custom Gradle Plugin in Kotlin
Gradle is a powerful tool, but let's be honest: most of us spend 90% of our time just copying snippets from StackOverflow into our build.gradle.kts files. But once your project grows—or you're managing a dozen microservices—you'll find yourself repeating the same build logic everywhere. That's when you stop writing scripts and start writing a custom plugin.
For this lesson, we're going to build a LicenseCheckerPlugin. The goal is simple: the build should fail if the project is missing a LICENSE file in the root directory. It's a small utility, but it perfectly illustrates how to hook into the Gradle lifecycle.
Setting up our buildSrc sandbox
Instead of creating a whole separate project and publishing it to MavenLocal, we'll use the buildSrc directory. If you haven't used it before, buildSrc is a special directory at your project root that Gradle compiles automatically and adds to the classpath of your main build scripts. It's the best place for internal plugins.
First, I'll create the directory structure: buildSrc/src/main/kotlin. Then, I need to tell buildSrc that it's actually a Kotlin-Gradle project. I'll add a build.gradle.kts inside the buildSrc folder:
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
The wrong way: checking during configuration
Now, let's write the plugin. My first instinct is usually to just put the logic inside the apply method. I'll try to check for the file right as the plugin is loaded.
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
class LicenseCheckerPlugin : Plugin<Project> {
override fun apply(project: Project) {
val licenseFile = File(project.projectDir, "LICENSE")
if (!licenseFile.exists()) {
throw GradleException("Build failed: LICENSE file is missing from the root directory!")
}
}
}
I ran this, and it worked... but it worked too well. I noticed that every single time I ran ./gradlew tasks or ./gradlew help, the build failed. This is a classic Gradle mistake: I put execution logic in the Configuration phase.
Gradle has a lifecycle. Configuration is when it builds the graph of tasks. Execution is when it actually runs them. By putting the check in apply, I'm forcing the check to happen every time Gradle even looks at the project. We only want this check to happen when we're actually building the project.
Doing it right with a dedicated Task
To fix this, I need to wrap my logic in a DefaultTask. This moves the check into the Execution phase. I'll define a task class and then register it within the plugin.
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.TaskAction
import java.io.File
abstract class CheckLicenseTask : DefaultTask() {
@TaskAction
fun check() {
val licenseFile = File(project.projectDir, "LICENSE")
if (!licenseFile.exists()) {
throw GradleException("Build failed: LICENSE file is missing from the root directory!")
}
println("License file found. We're good to go!")
}
}
class LicenseCheckerPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.register("checkLicense", CheckLicenseTask::class.java) {
group = "verification"
description = "Ensures a LICENSE file exists in the project root."
}
}
}
Now, running ./gradlew checkLicense works exactly as expected. If the file is missing, it crashes; if it's there, it prints a success message. More importantly, running other Gradle commands no longer triggers this check.
Hooking it into the build lifecycle
Running a manual task is fine, but we want this to be automatic. We want the checkLicense task to run whenever the project is checked (the standard check task in Java/Kotlin projects).
I'll update the apply method to create a dependency. I'm using dependsOn here to tell Gradle: "Before you run the check task, you must successfully run checkLicense."
override fun apply(project: Project) {
val checkLicense = project.tasks.register("checkLicense", CheckLicenseTask::class.java) {
group = "verification"
description = "Ensures a LICENSE file exists in the project root."
}
project.tasks.findByName("check")?.dependsOn(checkLicense)
}
Finally, to actually use this in the main project, I just need to apply it in the root build.gradle.kts. Since it's in buildSrc, I can just reference the class or give it an ID. For simplicity, I'll use the class approach:
apply<LicenseCheckerPlugin>()
Now, if you try to run ./gradlew check without a LICENSE file, Gradle will stop you dead in your tracks. That's the power of a custom plugin: you've codified a project requirement into the build process itself.
📋 Practical Task
Exercise: Build a Semantic Version Validator Plugin
Your goal is to create a custom Gradle plugin in buildSrc that validates the project version. Many companies require versions to follow a strict "Major.Minor.Patch" format (e.g., 1.0.4) and forbid versions like 1.0-SNAPSHOT or latest during release builds.
Requirements:
- Create a plugin called
VersionValidatorPlugin. - Register a task called
validateVersion. - The task should retrieve the project version using
project.version.toString(). - Use a Regular Expression to check if the version matches the pattern:
^\d+\.\d+\.\d+$(one or more digits, dot, one or more digits, dot, one or more digits). - If the version does not match, throw a
GradleExceptionwith a descriptive message explaining the required format. - Ensure the
validateVersiontask is automatically executed whenever thebuildtask is run.
There are no comments for now.