gradle-convention-plugin
Skill8 starsChanged 5 months agoApache-2.0View on GitHub
Creates and maintains Gradle Convention Plugins inside a build-logic included build for Android multi-module projects. Use when user asks to "create a convention plugin", "add a new plugin to build-logic", "share build config across modules", "reduce build.gradle duplication", or "centralize Gradle config".
In this repository14 other files it gives its agents.
- android-modularizationSkill
- android-permissions-editorSkill
- android-unit-test-editorSkill
- compose-editorSkill
- compose-navigation3Skill
- compose-performance-auditorSkill
- github-action-editorSkill
- gradle-configurationSkill
- koin-editorSkill
- kotlin-conventionSkill
- kotlin-coroutinesSkill
- mvi-editorSkill
- paparazzi-editorSkill
- review-owasp-securitySkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 2 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 11
Solution: Write config once in a plugin, apply it everywhere.
- Distinctive: line 179
Every plugin follows the same three-method structure:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Gradle Convention Plugin
Convention Plugins live in build-logic/ and centralize all shared build config. Each module's build.gradle.kts becomes a short list of plugin aliases + its own namespace.
Problem: Each module duplicates 100+ lines of SDK versions, compiler options, and test deps.
Solution: Write config once in a plugin, apply it everywhere.
Full Project Structure
root/
├── build-logic/
│ ├── settings.gradle.kts ← declares build-logic + shares version catalog
│ └── convention/
│ ├── build.gradle.kts ← kotlin-dsl + AGP/Kotlin deps + plugin registrations
│ └── src/main/kotlin/
│ ├── extensions/
│ │ ├── ProjectExtension.kt ← typed AGP accessors + libs catalog
│ │ └── DependencyHandlerExtension.kt ← string constants for dependency scopes
│ ├── AndroidLibraryConventionPlugin.kt
│ ├── AndroidApplicationConventionPlugin.kt
│ ├── ComposeConventionPlugin.kt
│ └── UnitTestConventionPlugin.kt
├── gradle/
│ └── libs.versions.toml
└── settings.gradle.kts ← includeBuild("build-logic")Step 1 — Wire build-logic
settings.gradle.kts (root)
pluginManagement {
includeBuild("build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}build-logic/settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml")) // single source of truth
}
}
}
rootProject.name = "build-logic"
include(":convention")Step 2 — build-logic/convention/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(libs.android.tools.build.gradle)
compileOnly(libs.kotlin.gradle)
}
gradlePlugin {
plugins {
register("androidLibrary") {
id = "my.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
register("androidApplication") {
id = "my.android.application"
implementationClass = "AndroidApplicationConventionPlugin"
}
register("compose") {
id = "my.compose"
implementationClass = "ComposeConventionPlugin"
}
register("unitTest") {
id = "my.test.unit"
implementationClass = "UnitTestConventionPlugin"
}
}
}Step 3 — Extension helpers (internal — only used inside build-logic)
extensions/ProjectExtension.kt
package extensions
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.dsl.CommonExtension
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
internal val javaVersion = JavaVersion.VERSION_21
internal val Project.libs: VersionCatalog
get() = extensions.getByType<VersionCatalogsExtension>().named("libs")
internal fun Project.libraryExtension(): LibraryExtension =
extensions.getByType(LibraryExtension::class)
internal fun Project.applicationExtension(): ApplicationExtension =
extensions.getByType(ApplicationExtension::class)
internal fun Project.commonExtension(): CommonExtension<*, *, *, *, *, *> =
extensions.getByType(CommonExtension::class)
internal fun Project.configureKotlinJvm() {
extensions.configure<JavaPluginExtension> {
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions.jvmTarget.set(JvmTarget.JVM_21)
}
}extensions/DependencyHandlerExtension.kt
package extensions
import org.gradle.api.artifacts.dsl.DependencyHandler
internal val DependencyHandler.implementation: String get() = "implementation"
internal val DependencyHandler.testImplementation: String get() = "testImplementation"
internal val DependencyHandler.androidTestImplementation: String get() = "androidTestImplementation"
internal val DependencyHandler.debugImplementation: String get() = "debugImplementation"
internal val DependencyHandler.ksp: String get() = "ksp"Step 4 — Plugin class anatomy
Every plugin follows the same three-method structure:
class MyConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
applyPlugins(project) // 1. apply Gradle/AGP plugins this plugin depends on
setProjectConfig(project) // 2. configure Android/Kotlin extensions
applyDependencies(project) // 3. add required dependencies
}
private fun applyPlugins(project: Project) { ... }
private fun setProjectConfig(project: Project) { ... }
private fun applyDependencies(project: Project) { ... }
}Not every plugin needs all three — a simple tool plugin (serialization, collections) may only need
applyPlugins+applyDependencies.
Plugin Examples
AndroidLibraryConventionPlugin.kt
import extensions.configureKotlinJvm
import extensions.implementation
import extensions.javaVersion
import extensions.libraryExtension
import extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
applyPlugins(project)
setProjectConfig(project)
applyDependencies(project)
}
private fun applyPlugins(project: Project) {
project.apply(plugin = "com.android.library")
}
private fun setProjectConfig(project: Project) {
project.libraryExtension().apply {
compileSdk = project.libs.findVersion("compile.sdk").get().toString().toInt()
defaultConfig {
minSdk = project.libs.findVersion("min.sdk").get().toString().toInt()
}
compileOptions {
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
}
buildTypes {
release {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
packaging {
resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" }
}
}
project.configureKotlinJvm()
}
private fun applyDependencies(project: Project) {
project.dependencies {
add(implementation, project.libs.findLibrary("androidx.core.ktx").get())
}
}
}ComposeConventionPlugin.kt
import extensions.androidTestImplementation
import extensions.commonExtension
import extensions.debugImplementation
import extensions.implementation
import extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies
class ComposeConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
applyPlugins(project)
setProjectConfig(project)
applyDependencies(project)
}
private fun applyPlugins(project: Project) {
// Read plugin ID from catalog — avoids hardcoding
val pluginId = project.libs.findPlugin("kotlin.compose").get().get().pluginId
project.apply(plugin = pluginId)
}
private fun setProjectConfig(project: Project) {
project.commonExtension().buildFeatures.compose = true
}
private fun applyDependencies(project: Project) {
project.dependencies {
val bom = project.libs.findLibrary("androidx.compose.bom").get()
add(implementation, platform(bom))
add(implementation, project.libs.findLibrary("androidx.compose.ui").get())
add(implementation, project.libs.findLibrary("androidx.compose.material3").get())
add(debugImplementation, project.libs.findLibrary("androidx.compose.ui.tooling").get())
add(androidTestImplementation, platform(bom))
}
}
}UnitTestConventionPlugin.kt
import extensions.libs
import extensions.testImplementation
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.withType
class UnitTestConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
applyDependencies(project)
project.tasks.withType<Test>().configureEach { useJUnitPlatform() }
}
private fun applyDependencies(project: Project) {
project.dependencies {
add(testImplementation, project.libs.findLibrary("test.junit").get())
add(testImplementation, project.libs.findLibrary("test.mockk").get())
add(testImplementation, project.libs.findLibrary("test.coroutine").get())
add(testImplementation, project.libs.findLibrary("turbine").get())
}
}
}Minimal tool plugin (no AGP — serialization, immutable collections, etc.)
import extensions.implementation
import extensions.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies
class SerializationConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
val pluginId = project.libs.findPlugin("kotlin.serialization").get().get().pluginId
project.apply(plugin = pluginId)
project.dependencies {
add(implementation, project.libs.findLibrary("jetbrains.kotlin.serialization").get())
}
}
}Step 5 — Declare aliases in libs.versions.toml
Convention plugins from an included build have no real version — use version = "unspecified":
[plugins]
# Third-party plugins (real versions)
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
# Convention plugins — MUST use version = "unspecified"
my-android-library = { id = "my.android.library", version = "unspecified" }
my-android-app = { id = "my.android.application", version = "unspecified" }
my-compose = { id = "my.compose", version = "unspecified" }
my-unit-test = { id = "my.test.unit", version = "unspecified" }Step 6 — Apply in a module
// feature/my-feature/build.gradle.kts
plugins {
alias(libs.plugins.my.android.library)
alias(libs.plugins.my.compose)
alias(libs.plugins.my.unit.test)
}
android {
namespace = "com.example.feature.myfeature"
}
dependencies {
implementation(project(":component:my-component"))
}When to Create a New Plugin
- Copy-pasting 3+ lines across multiple
build.gradle.kts - New tool added project-wide (Detekt, Jacoco, Paparazzi)
- New module type introduced (e.g.,
integration,common-ui) - Existing plugin has two unrelated concerns → split into two
One plugin per concern: Compose, UnitTest, and Detekt are separate plugins so each module opts in independently.
Rules
| ✅ Do | ❌ Don't |
Read versions via libs.findVersion("key").get().toString() | Hardcode compileSdk = 35 in plugin code |
Read libraries via libs.findLibrary("key").get() | Use string coordinates "group:artifact:1.0" |
Read plugin IDs via libs.findPlugin("key").get().get().pluginId | Hardcode plugin IDs as strings |
version = "unspecified" for convention plugin aliases | Omit the version field (causes resolution error) |
internal for all extension helpers | Expose extension helpers as public |
| One plugin per concern | Bundle unrelated concerns in one plugin |
Checklist: New Convention Plugin
- Plugin class in
build-logic/convention/src/main/kotlin/ - Follows
applyPlugins / setProjectConfig / applyDependenciesstructure - All versions/libraries/plugins read via
project.libs.find*— no hardcoded strings - Extension helpers used (
libraryExtension(),commonExtension(), scope constants) - Registered in
build-logic/convention/build.gradle.ktswith a stable ID - Alias declared in
libs.versions.tomlwithversion = "unspecified" - Applied in at least one module —
./gradlew assembleDebugpasses
References
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

