Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Benevolent Gradle Overlord: Keeping Order

The Benevolent Gradle Overlord: Keeping Order

Talk by Aurimas Liutikas at Droidcon London 2025

Avatar for Aurimas Liutikas

Aurimas Liutikas

October 31, 2025
Tweet

More Decks by Aurimas Liutikas

Other Decks in Programming

Transcript

  1. plugins { id("com.android.library") id("org.jetbrains.kotlin.android") } android { namespace = "com.example.mylibrary"

    compileSdk = 36 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = "11" } } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) }
  2. Leave only dependencies {} and custom extension plugins { id("FancyPlugin")

    id("com.android.library") } dependencies { implementation("foo:bar:1.0.0") } fancy { allowedToggleFoo = true
  3. Add reviewers for change patterns For changes outside of dependencies

    and fancy DSLs automatically add reviewers from the build team banana { allowedToggleFoo = true allowedToggleBar = true } android { compileSdk = 36 }
  4. banana { allowedToggleFoo = true allowedToggleBar = true } android

    { compileSdk = 36 } Add reviewers for change patterns REST API call /repos/{owner}/{repo}/ pulls/{pull_number}/ requested_reviewers
  5. disallowChanges on DSL properties private fun Project.configureKotlin() { val kotlin

    = extensions.getByType( KotlinAndroidExtension::class.java ) kotlin.compilerOptions.jvmTarget.setAndDisallow( JvmTarget.JVM_1_8 ) }
  6. disallowChanges on DSL properties FAILURE: Build failed with an exception.

    * Where: Build file 'your-build-your-rules/build.gradle.kts' line: 45 * What went wrong: The value for property 'jvmTarget' cannot be changed any further. ❌
  7. finalizeDsl for Android projects private fun Project.configureAppPlugin() { val android

    = extensions.getByType( ApplicationAndroidComponentsExtension::class.java ) android.finalizeDsl { it.compileSdk = 36 } }
  8. Last resort afterEvaluate private fun Project.configureJavaPlugin() { val java =

    extensions.getByType( JavaPluginExtension::class.java) afterEvaluate { java.sourceCompatibility = JavaVersion.VERSION_1_8 java.targetCompatibility = JavaVersion.VERSION_1_8 } }
  9. afterEvaluate { androidComponents.finalizeDsl { it.compileSdk = 35 } } afterEvaluate

    { afterEvaluate { java.sourceCompatibility = JavaVersion.VERSION_17 java.targetCompatibility = JavaVersion.VERSION_17 } }
  10. Extension validation abstract class DslValidationTask : DefaultTask() { @get:Input abstract

    val testRunner: Property<String> @TaskAction fun validate() { if (testRunner.get() != EXPECTED_TEST_RUNNER) throw Exception("Use $EXPECTED_TEST_RUNNER!") } } private const val EXPECTED_TEST_RUNNER = "androidx.test..."
  11. private fun Project.configureAppPlugin() { val android = extensions.getByType( ApplicationExtension::class.java) val

    testRunner = provider { android.defaultConfig.testInstrumentationRunner } tasks.register( "validateDsl", DslValidationTask::class.java) { it.testRunner.set(testRunner) } }
  12. Extension validation android { defaultConfig { testInstrumentationRunner = "com.BadRunner" }

    } * What went wrong: Execution failed for task ':app:validateDsl'. > java.lang.Exception: Use androidx.test.runner.AndroidJUnitRunner ❌
  13. Task output validation abstract class ValidateManifestTask : DefaultTask() { @get:InputFile

    abstract val manifest: RegularFileProperty @TaskAction fun validate() { if (!manifest.get().asFile.readText().contains( """<uses-sdk android:minSdkVersion="24" />""" )) throw Exception("minSdkVersion was not set to 24") } }
  14. Task output validation private fun Project.configureAndroidLibraryPlugin() { extensions.getByType( LibraryAndroidComponentsExtension::class.java ).onVariants

    { tasks.register("validateManifest${it.name}", ValidateManifestTask::class.java) { task -> task.manifest.set( it.artifacts.get(SingleArtifact.MERGED_MANIFEST)) } } }
  15. Task output validation android { defaultConfig { minSdk = 30

    } } * What went wrong: Execution failed for task ':lib:validateManifestdebug'. > java.lang.Exception: minSdkVersion was not set to 24 ❌
  16. build.gradle.kts content validation abstract class ValidateBuildGradleTask : DefaultTask() { @get:InputFile

    abstract val build: RegularFileProperty @TaskAction fun validate() { val buildContents = build.get().asFile.readText() if (buildContents.contains("kotlin {")) throw Exception("Do not configure Kotlin ...") } }
  17. build.gradle.kts content validation kotlin { // any configuration } *

    What went wrong: Execution failed for task ':app:validateBuildGradle'. > java.lang.Exception: Do not configure Kotlin plugin directly. ❌
  18. Move to a custom build.json definition file { "type": "androidLibrary",

    "dependencies": { "implementation": [ "androidx.annotation:annotation:1.9.1" ] } }
  19. Basic model of JSON using moshi @JsonClass(generateAdapter = true) data

    class BuildFile( val type: ProjectType, val dependencies: Dependencies, ) enum class ProjectType { androidLibrary, androidApplication } @JsonClass(generateAdapter = true) data class Dependencies(val implementation: List<String>)
  20. Parse the JSON class JsonProjectPlugin : Plugin<Project> { override fun

    apply(project: Project) { val buildJsonFile = project.layout.projectDirectory.file("build.json").asFile val moshi: Moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter<BuildFile>() val build = jsonAdapter.fromJson(buildJsonFile.readText())!! // ..
  21. Configure based on build.json class JsonProjectPlugin : Plugin<Project> { override

    fun apply(project: Project) { // .. val build = jsonAdapter.fromJson(buildJsonFile.readText())!! when (build.type) { ProjectType.androidLibrary -> project.library(build) ProjectType.androidApplication -> project.app(build) } }
  22. Configure based on build.json private fun Project.library(build: BuildFile) { configureKotlin()

    plugins.apply("com.android.library") configureAndroidCommon() build.dependencies.implementation.forEach { dep -> dependencies.add("implementation", dep) } }
  23. Other formats - XML <?xml version="1.0" encoding="UTF-8"?> <androidLibrary> <dependencies> <implementation>

    androidx.annotation:annotation:1.9.1 </implementation> </dependencies> </androidLibrary>
  24. Declarative Gradle javaApplication { javaVersion = 21 mainClass = "com.example.App"

    dependencies { implementation(project(":java-util")) implementation("com.google.guava:guava:32.1.3-jre") } }
  25. Things to keep up to date JDK Gradle Android Gradle

    Plugin Kotlin Gradle Plugin Kotlin Symbol Processors
  26. Things to keep up to date JDK Gradle Android Gradle

    Plugin Kotlin Gradle Plugin Kotlin Symbol Processors Testing tools
  27. Things to keep up to date JDK Gradle Android Gradle

    Plugin Kotlin Gradle Plugin Kotlin Symbol Processors Testing tools …
  28. Skip-version upgrades are harder favoriteApi = true @Deprecated( “use betterApi”)

    favoriteApi = true betterApi = true betterApi = true AGP 8.4.3 AGP 8.13.0 AGP 9.0.0
  29. Static analysis FTW Android Lint (for both Android and JVM

    code) detekt ktlint Error-prone nebula.lint
  30. Pick relevant metrics Inner loop cycle time Time from upload

    to merge Days behind latest Gradle version
  31. Pick relevant metrics Inner loop cycle time Time from upload

    to merge Days behind latest Gradle version CSAT
  32. Takeaways Pick the strictness based on the severity of making

    a mistake build.gradle(.kts) is not required to configure projects
  33. Takeaways Pick the strictness based on the severity of making

    a mistake build.gradle(.kts) is not required to configure projects Be proactive about your upgrades
  34. Takeaways Pick the strictness based on the severity of making

    a mistake build.gradle(.kts) is not required to configure projects Be proactive about your upgrades Give feedback to tool & library owners
  35. Takeaways Pick the strictness based on the severity of making

    a mistake build.gradle(.kts) is not required to configure projects Be proactive about your upgrades Give feedback to tool & library owners Automate enforcement using tasks and static analysis
  36. Takeaways Pick the strictness based on the severity of making

    a mistake build.gradle(.kts) is not required to configure projects Be proactive about your upgrades Give feedback to tool & library owners Automate enforcement using tasks and static analysis Monitor your impact to gain social capital
  37. Takeaways Pick the strictness based on the severity of making

    a mistake build.gradle(.kts) is not required to configure projects Be proactive about your upgrades Give feedback to tool & library owners Automate enforcement using tasks and static analysis Monitor your impact to gain social capital Profit £££