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

Scaling your build logic

Scaling your build logic

Presented at Londroid on 2025.01.16

This presentation explores strategies for managing and extracting common build logic across different project phases, from pre-compiled scripts to convention plugins and defining your very own custom DSL to hide away the complexity of third party dependencies and plugins.

Footnotes:
[^1]: This presentation will focus on Gradle as a build tool.
[^2]: https://blog.gradle.org/declarative-gradle#software-definition-vs-build-logic
[^3]: Always consider the stage of the project. Just as to any approach there are always caveats.
[^4]: https://github.com/cdsap/ProjectGenerator
[^5]: https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:convention_plugins_vs_cross_configuration
[^6]: https://docs.gradle.org/current/userguide/implementing_gradle_plugins_precompiled.html
[^7]: Unless you use `--no-rebuild`
[^8]: https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:using_buildsrc
[^9]: https://medium.com/proandroiddev/stop-using-gradle-buildsrc-use-composite-builds-instead-3c38ac7a2ab3
[^10]: https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes
[^11]: https://developer.android.com/reference/tools/gradle-api/8.0/com/android/build/api/dsl/SettingsExtension
[^12]: https://github.com/anthonymonori/sample-gradle-project/pull/1/files
[^13]: https://developer.squareup.com/blog/herding-elephants/
[^14]: https://github.com/android/nowinandroid
[^15]: https://github.com/slackhq/foundry

Antal Monori

January 16, 2025
Tweet

Other Decks in Programming

Transcript

  1. Definition build logic (n.) A set of declarative instructions and

    plugins that transform source code and resources into deployable packages.
  2. Definition build logic (n.) A set of declarative instructions and

    plugins that transform source code and resources into deployable packages. "Build logic is how the software will be built. It is the code that adds new capabilities, integrates different tools, and supplies conventions to the software definition."2 2 https://blog.gradle.org/declarative-gradle#software-definition-vs-build-logic
  3. Goals 1. Some Gradle fundamentals 2. How to create a

    Gradle plugin 3. Pre-compiled scripts vs convention plugins
  4. Goals 1. Some Gradle fundamentals 2. How to create a

    Gradle plugin 3. Pre-compiled scripts vs convention plugins 4. buildSrc vs includedBuild()
  5. Goals 1. Some Gradle fundamentals 2. How to create a

    Gradle plugin 3. Pre-compiled scripts vs convention plugins 4. buildSrc vs includedBuild() 5. How to build a declarative, custom DSL
  6. Disclaimer3 3 Always consider the stage of the project. Just

    as to any approach there are always caveats.
  7. Let's create a more realistic example4 $ projectGenerator --shape triangle

    --layers 5 --modules 100 --classes-module-type random --classes-module 150 --language kts --agp-version 8.7.2 --kgp-version 2.1.0 --type-of-string-resources large --generate-unit-test true --gradle 8.11.1 --dependency-plugins true 4 https://github.com/cdsap/ProjectGenerator
  8. Let's create a more realistic example4 $ projectGenerator --shape triangle

    --layers 5 --modules 100 --classes-module-type random --classes-module 150 --language kts --agp-version 8.7.2 --kgp-version 2.1.0 --type-of-string-resources large --generate-unit-test true --gradle 8.11.1 --dependency-plugins true 4 https://github.com/cdsap/ProjectGenerator
  9. A more realistic example // settings.gradle.kts rootProject.name="sample-gradle-project" include (":app") include

    (":layer_0:module_0_1") include (":layer_0:module_0_2") include (":layer_0:module_0_3") include (":layer_0:module_0_4") include (":layer_0:module_0_5") include (":layer_0:module_0_6") include (":layer_0:module_0_7") include (":layer_0:module_0_8") include (":layer_0:module_0_9") include (":layer_0:module_0_10") include (":layer_0:module_0_11") include (":layer_0:module_0_12") include (":layer_0:module_0_13") include (":layer_0:module_0_14") include (":layer_0:module_0_15") include (":layer_0:module_0_16") include (":layer_1:module_1_17") include (":layer_1:module_1_18") include (":layer_1:module_1_19") include (":layer_1:module_1_20") // ...
  10. With many build scripts plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlin-kapt") }

    val normalizedName = name.replace(":", "_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  11. With many build scripts plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlin-kapt") }

    val normalizedName = name.replace(":", "_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  12. With many build scripts plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlin-kapt") }

    val normalizedName = name.replace(":", "_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  13. With many build scripts plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlin-kapt") }

    val normalizedName = name.replace(":", "_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  14. With many build scripts plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlin-kapt") }

    val normalizedName = name.replace(":", "_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  15. With many build scripts plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlin-kapt") }

    val normalizedName = name.replace(":", "_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  16. Growing pains This is when the real growing pains start

    to show their face. • Duplication across build scripts
  17. Growing pains This is when the real growing pains start

    to show their face. • Duplication across build scripts • Misconfiguration going unnoticed
  18. Growing pains This is when the real growing pains start

    to show their face. • Duplication across build scripts • Misconfiguration going unnoticed • Larger surface area to maintain
  19. Growing pains This is when the real growing pains start

    to show their face. • Duplication across build scripts • Misconfiguration going unnoticed • Larger surface area to maintain • Tedious and slow iterations/migrations
  20. Let's start extracting // layer_1/module_1_20/build.gradle.kts plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlin-kapt")

    } val normalizedName = name.replace(":", "_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  21. Applying what the internet says // build.gradle.kts subprojects { android

    { compileSdk = 33 defaultConfig { minSdk = 24 } } }
  22. Applying what the internet says // build.gradle.kts subprojects { android

    { // ERROR: Unresolved reference 'android'. compileSdk = 33 defaultConfig { minSdk = 24 } } }
  23. Yes, we can fix this... // build.gradle.kts subprojects { plugins.withId("com.android.application")

    { configureAndroid(project) } plugins.withId("com.android.library") { configureAndroid(project) } } fun configureAndroid(project: Project) { project.extensions.configure<com.android.build.gradle.BaseExtension>("android") { compileSdkVersion(33) defaultConfig { minSdk = 24 } } }
  24. Tip #1 Avoid using allprojects {} and subprojects {} because:

    • Breaks isolation5 5 https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:convention_plugins_vs_cross_configuration
  25. Tip #1 Avoid using allprojects {} and subprojects {} because:

    • Breaks isolation5 • Performance hit 5 https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:convention_plugins_vs_cross_configuration
  26. Tip #1 Avoid using allprojects {} and subprojects {} because:

    • Breaks isolation5 • Performance hit • Hard to debug 5 https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:convention_plugins_vs_cross_configuration
  27. Tip #1 Avoid using allprojects {} and subprojects {} because:

    • Breaks isolation • Performance hit • Hard to debug Instead Use pre-compiled scripts or convention plugins
  28. What are they A pre-compiled script6 refers to a reusable

    build script file, that encapsulates shared build logic (such as configurations and dependencies) for consistent use across multiple projects. 6 https://docs.gradle.org/current/userguide/implementing_gradle_plugins_precompiled.html
  29. What are they A pre-compiled script refers to a reusable

    build script file, that encapsulates shared build logic (such as configurations and dependencies) for consistent use across multiple projects. A convention plugin refers to a reusable Gradle binary plugin, that encapsulates shared build logic (such as configurations and dependencies) for consistent use across multiple projects.
  30. Are they the same? Both pre-compiled scripts and convention plugins:

    • Written in Groovy or Kotlin DSL • Encapsulate common, shared build logic
  31. Are they the same? Both pre-compiled scripts and convention plugins:

    • Written in Groovy or Kotlin DSL • Encapsulate common, shared build logic • Are simply plugins after all
  32. Pre-compiled scripts $ mkdir buildSrc $ touch buildSrc/build.gradle.kts // buildSrc/build.gradle.kts

    plugins { `kotlin-dsl` `kotlin-dsl-precompiled-script-plugins` // Enables Kotlin DSL for precompiled script plugins } dependencies { } repositories { google() mavenCentral() gradlePluginPortal() }
  33. Let's take a library build script plugins { id("com.android.library") id("org.jetbrains.kotlin.android")

    id("kotlin-kapt") } val normalizedName = name.replace(":","_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  34. sample.android.library.gradle.kts plugins { id("com.android.library") id("org.jetbrains.kotlin.android") id("kotlin-kapt") } val normalizedName =

    name.replace(":","_") android { namespace = "com.example.mylibrary.$normalizedName" compileSdk = 33 defaultConfig { minSdk = 24 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }
  35. The generated code // buildSrc/build/generated-sources/kotlin-dsl-plugins/kotlin/Sample_android_libraryPlugin.kt public class Sample_android_libraryPlugin : org.gradle.api.Plugin<org.gradle.api.Project>

    { override fun apply(target: org.gradle.api.Project) { try { Class .forName("Sample_android_library_gradle") .getDeclaredConstructor( org.gradle.api.Project::class.java, org.gradle.api.Project::class.java ) .newInstance(target, target) } catch (e: java.lang.reflect.InvocationTargetException) { throw e.targetException } } }
  36. What about the root project? // build.gradle.kts plugins { id("org.jetbrains.kotlin.jvm")

    version("2.1.0") apply false id("com.android.application") version ("8.7.2") apply false id("com.android.library") version ("8.7.2") apply false }
  37. Where to pin them instead? // buildSrc/build.gradle.kts plugins { `kotlin-dsl`

    `kotlin-dsl-precompiled-script-plugins` } dependencies { implementation("com.android.tools.build:gradle:8.7.2") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") } repositories { google() mavenCentral() gradlePluginPortal() }
  38. Tip #2 Avoid using buildSrc: • It's a special directory,

    only one can exist • Added to the root project classpath
  39. Tip #2 Avoid using buildSrc: • It's a special directory,

    only one can exist • Added to the root project classpath • Any change7 causes the whole project to become out-of-date8 8 https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html#sec:using_buildsrc 7 Unless you use --no-rebuild
  40. Tip #2 Avoid using buildSrc: • It's a special directory,

    only one can exist • Added to the root project classpath • Any change causes the whole project to become out-of-date Instead Use composite builds (also known as includedBuild())9 9 https://medium.com/proandroiddev/stop-using-gradle-buildsrc-use-composite-builds-instead-3c38ac7a2ab3
  41. Migrating to composite builds $ mv buildSrc build-logic // settings.gradle.kts

    rootProject.name="sample-gradle-project" includeBuild ("build-logic") include (":app") include (":layer_0:module_0_1") include (":layer_0:module_0_2") include (":layer_0:module_0_3") include (":layer_0:module_0_4") include (":layer_0:module_0_5") include (":layer_0:module_0_6") include (":layer_0:module_0_7") include (":layer_0:module_0_8") include (":layer_0:module_0_9") // ...
  42. Migrating to convention plugins // build-logic/build.gradle.kts plugins { `kotlin-dsl` `kotlin-dsl-precompiled-script-plugins`

    // Enables Kotlin DSL for precompiled script plugins } dependencies { implementation("com.android.tools.build:gradle:8.7.2") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") } repositories { google() mavenCentral() gradlePluginPortal() }
  43. Migrating to convention plugins // build-logic/build.gradle.kts plugins { `kotlin-dsl` }

    dependencies { implementation("com.android.tools.build:gradle:8.7.2") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") } gradlePlugin { plugins { } } repositories { google() mavenCentral() gradlePluginPortal() }
  44. Migrating to convention plugins // build-logic/build.gradle.kts plugins { /* ..

    */} dependencies { /* .. */} gradlePlugin { plugins { create("root") { id = "sample.$name" implementationClass = "convention.SampleRootPlugin" version = "1.0.0" } create("android.library") { id = "sample.$name" implementationClass = "convention.SampleAndroidLibraryPlugin" version = "1.0.0" } create("android.application") { id = "sample.$name" implementationClass = "convention.SampleAndroidApplicationPlugin" version = "1.0.0" } } } repositories { /* .. */ }
  45. Migrating to convention plugins // build-logic/src/main/kotlin/convention/SampleRootPlugin.kt internal class SampleRootPlugin :

    Plugin<Project> { override fun apply(project: Project) { require(project == project.rootProject) { "Plugin must be applied to root project!" } } }
  46. Migrating to convention plugins internal class SampleAndroidApplicationPlugin : Plugin<Project> {

    override fun apply(project: Project) { with(project) { plugins.apply("com.android.library") plugins.apply("org.jetbrains.kotlin.android") plugins.apply("kotlin-kapt") extensions.getByType(ApplicationExtension::class.java).apply { compileSdk = 33 defaultConfig { applicationId = "com.example.myapplication" minSdk = 24 targetSdk = 33 } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies.apply { add("implementation", "androidx.appcompat:appcompat:1.6.1") // ... } } } }
  47. Migrating to convention plugins internal class SampleAndroidApplicationPlugin : Plugin<Project> {

    override fun apply(project: Project) { with(project) { plugins.apply("com.android.library") plugins.apply("org.jetbrains.kotlin.android") plugins.apply("kotlin-kapt") extensions.getByType(ApplicationExtension::class.java).apply { compileSdk = 33 defaultConfig { applicationId = "com.example.myapplication" minSdk = 24 targetSdk = 33 } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } } tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies.apply { add("implementation", "androidx.appcompat:appcompat:1.6.1") // ... } } } }
  48. Achievements, so far... • We went from 1000s of lines

    to 100s of lines of code • Spreading over in 100s of projects to 1
  49. Achievements, so far... • We went from 1000s of lines

    to 100s of lines of code • Spreading over in 100s of projects to 1 • We now own many Gradle plugins (locally)
  50. There's more... // settings.gradle.kts rootProject.name="sample-gradle-project" includeBuild ("build-logic") include (":app") include

    (":layer_0:module_0_1") include (":layer_0:module_0_2") include (":layer_0:module_0_3") include (":layer_0:module_0_4") include (":layer_0:module_0_5") include (":layer_0:module_0_6") include (":layer_0:module_0_7") include (":layer_0:module_0_8") include (":layer_0:module_0_9") include (":layer_0:module_0_10") include (":layer_0:module_0_11") // ...
  51. There's more... // settings.gradle.kts // ... enableFeaturePreview("STABLE_CONFIGURATION_CACHE") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement {

    repositories { google().content { includeGroupByRegex("com\\.google.*") includeGroupByRegex("com\\.android.*") includeGroupByRegex("androidx.*") } mavenCentral() } } buildCache { local { isEnabled = true } }
  52. There's logic in settings kts // settings.gradle.kts // ... enableFeaturePreview("STABLE_CONFIGURATION_CACHE")

    enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { repositories { google().content { includeGroupByRegex("com\\.google.*") includeGroupByRegex("com\\.android.*") includeGroupByRegex("androidx.*") } mavenCentral() } } buildCache { local { isEnabled = true } }
  53. There's logic in settings kts // settings.gradle.kts // ... enableFeaturePreview("STABLE_CONFIGURATION_CACHE")

    enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { repositories { google().content { includeGroupByRegex("com\\.google.*") includeGroupByRegex("com\\.android.*") includeGroupByRegex("androidx.*") } mavenCentral() } } buildCache { local { isEnabled = true } }
  54. There's logic in settings kts // settings.gradle.kts // ... enableFeaturePreview("STABLE_CONFIGURATION_CACHE")

    enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { repositories { google().content { includeGroupByRegex("com\\.google.*") includeGroupByRegex("com\\.android.*") includeGroupByRegex("androidx.*") } mavenCentral() } } buildCache { local { isEnabled = true } }
  55. Creating a settings convention plugin $ mkdir build-logic-settings $ touch

    build-logic-settings/build.gradle.kts plugins { `kotlin-dsl` } gradlePlugin { plugins { create("settings") { id = "sample.$name" implementationClass = "convention.SampleSettingsPlugin" } } } repositories { gradlePluginPortal() }
  56. Creating a settings convention plugin // build-logic-settings/src/main/kotlin/convention/SampleSettingsPlugin.kt class SampleSettingsPlugin :

    Plugin<Settings> { override fun apply(settings: Settings) { with(settings) { enableFeaturePreview("STABLE_CONFIGURATION_CACHE") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { configurePluginManagement() } dependencyResolutionManagement { configureRepositories() } pluginManager.apply(GradleBuildCachePlugin::class.java) } } }
  57. Creating a settings convention plugin // build-logic-settings/src/main/kotlin/convention/SampleSettingsPlugin.kt class SampleSettingsPlugin :

    Plugin<Settings> { override fun apply(settings: Settings) { with(settings) { enableFeaturePreview("STABLE_CONFIGURATION_CACHE") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { configurePluginManagement() } dependencyResolutionManagement { configureRepositories() } pluginManager.apply(GradleBuildCachePlugin::class.java) } } }
  58. Creating a settings convention plugin // build-logic-settings/src/main/kotlin/convention/SampleSettingsPlugin.kt class SampleSettingsPlugin :

    Plugin<Settings> { override fun apply(settings: Settings) { with(settings) { enableFeaturePreview("STABLE_CONFIGURATION_CACHE") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { configurePluginManagement() } dependencyResolutionManagement { configureRepositories() } pluginManager.apply(GradleBuildCachePlugin::class.java) } } }
  59. Tip #3 Android Grade Plugin 8.0.010 now ships with a

    settings plugin artifact: plugins { id("com.android.settings") version "<agp-version>" } Then we can tweak a few props via the android11 extension. 11 https://developer.android.com/reference/tools/gradle-api/8.0/com/android/build/api/dsl/SettingsExtension 10 https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes
  60. Apply to settings convention plugin plugins { `kotlin-dsl` } dependencies

    { implementation("com.android.settings:com.android.settings.gradle.plugin:8.7.2") } gradlePlugin { plugins { create("settings") { id = "sample.$name" implementationClass = "convention.SampleSettingsPlugin" } } } repositories { google() gradlePluginPortal() }
  61. Apply to settings convention plugin // build-logic-settings/src/main/kotlin/convention/SampleSettingsPlugin.kt import com.android.build.api.dsl.SettingsExtension import

    com.android.build.gradle.SettingsPlugin class SampleSettingsPlugin : Plugin<Settings> { override fun apply(settings: Settings) { with(settings) { // ... pluginManager.apply(SettingsPlugin::class.java) extensions.getByType<SettingsExtension>().apply { minSdk = 24 compileSdk = 33 } } } }
  62. Recap We now have: 1. Understand how to structure build

    logic 2. Creating and applying convention plugins
  63. But I use Compose UI... // layer_1/module_1_19/build.gradle.kts plugins { id("sample.android.library")

    id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.compose") } android { buildFeatures { compose = true } } dependencies { debugImplementation("org.jetbrains.compose.ui:ui-tooling-preview:1.7.1") debugImplementation("org.jetbrains.compose.components:components-ui-tooling-preview:1.7.1") implementation("org.jetbrains.compose.ui:ui:1.7.1") implementation("org.jetbrains.compose.runtime:runtime:1.7.1") implementation("org.jetbrains.compose.material3:material3:1.7.1") implementation("org.jetbrains.compose.foundation:foundation:1.7.1") implementation("org.jetbrains.compose.ui:ui-tooling:1.7.1") }
  64. But I use Dagger Hilt... // layer_1/module_1_21/build.gradle.kts plugins { id("sample.android.library")

    id("kotlin-kapt") id("com.google.dagger.hilt.android") } dependencies { implementation("com.google.dagger:hilt-android:2.53.1") kapt("com.google.dagger:hilt-compiler:2.53.1") } kapt { correctErrorTypes = true }
  65. How do I implement it12 1. Create a base plugin

    12 https://github.com/anthonymonori/sample-gradle-project/pull/1/files
  66. How do I implement it12 1. Create a base plugin

    2. Create a @DslMarker annotation 12 https://github.com/anthonymonori/sample-gradle-project/pull/1/files
  67. How do I implement it12 1. Create a base plugin

    2. Create a @DslMarker annotation 3. Create a core DSL class sample { } 12 https://github.com/anthonymonori/sample-gradle-project/pull/1/files
  68. How do I implement it12 1. Create a base plugin

    2. Create a @DslMarker annotation 3. Create a core DSL class sample { } 4. Create a nested feature DSL class features { } 12 https://github.com/anthonymonori/sample-gradle-project/pull/1/files
  69. How do I implement it12 1. Create a base plugin

    2. Create a @DslMarker annotation 3. Create a core DSL class sample { } 4. Create a nested feature DSL class features { } 5. Create the features 12 https://github.com/anthonymonori/sample-gradle-project/pull/1/files
  70. Set up the base plugin // build-logic/src/main/kotlin/com/sample/SampleBasePlugin.kt class SampleBasePlugin :

    Plugin<Project> { override fun apply(project: Project) { with(project) { val sampleExtension = extensions.create("sample", SampleCompanyExtension::class.java) pluginManager.withPlugin("com.android.application") { configure<BaseAppModuleExtension> { sampleExtension.androidExtension = this } } project.pluginManager.withPlugin("com.android.library") { configure<LibraryExtension> { sampleExtension.androidExtension = this } } sampleExtension.applyTo(this) } } }
  71. Set up the base plugin // build-logic/src/main/kotlin/com/sample/SampleBasePlugin.kt class SampleBasePlugin :

    Plugin<Project> { override fun apply(project: Project) { with(project) { val sampleExtension = extensions.create("sample", SampleCompanyExtension::class.java) pluginManager.withPlugin("com.android.application") { configure<BaseAppModuleExtension> { sampleExtension.androidExtension = this } } project.pluginManager.withPlugin("com.android.library") { configure<LibraryExtension> { sampleExtension.androidExtension = this } } sampleExtension.applyTo(this) } } }
  72. Set up the marker and core DSL // build-logic/src/main/kotlin/com/sample/SampleCompanyHandler.kt @DslMarker

    annotation class SampleCompanyExtensionMarker @SampleCompanyExtensionMarker open class SampleCompanyExtension @Inject internal constructor(objects: ObjectFactory) { private val featuresHandler = objects.newInstance(FeaturesHandler::class.java) fun features(action: Action<FeaturesHandler>) { action.execute(featuresHandler) } internal fun applyTo(project: Project) { project.afterEvaluate { featuresHandler.applyTo(project) } } }
  73. Set up the marker and core DSL // build-logic/src/main/kotlin/com/sample/SampleCompanyHandler.kt @DslMarker

    annotation class SampleCompanyExtensionMarker @SampleCompanyExtensionMarker open class SampleCompanyExtension @Inject internal constructor(objects: ObjectFactory) { private val featuresHandler = objects.newInstance(FeaturesHandler::class.java) fun features(action: Action<FeaturesHandler>) { action.execute(featuresHandler) } internal fun applyTo(project: Project) { project.afterEvaluate { featuresHandler.applyTo(project) } } }
  74. Set up the marker and core DSL // build-logic/src/main/kotlin/com/sample/SampleCompanyHandler.kt @DslMarker

    annotation class SampleCompanyExtensionMarker @SampleCompanyExtensionMarker open class SampleCompanyExtension @Inject internal constructor(objects: ObjectFactory) { private val featuresHandler = objects.newInstance(FeaturesHandler::class.java) fun features(action: Action<FeaturesHandler>) { action.execute(featuresHandler) } internal fun applyTo(project: Project) { project.afterEvaluate { featuresHandler.applyTo(project) } } }
  75. Set up the feature DSL // build-logic/src/main/kotlin/com/sample/features/FeatureHandler.kt @SampleCompanyExtensionMarker abstract class

    FeaturesHandler @Inject internal constructor(objects: ObjectFactory) { private val composeHandler = objects.newInstance<ComposeHandler>() private val daggerHandler = objects.newInstance<DaggerHandler>() internal fun applyTo(project: Project) { composeHandler.applyTo(project) daggerHandler.applyTo(project) } fun compose(action: Action<ComposeHandler>? = null) { composeHandler.enable() action?.execute(composeHandler) } fun dagger(action: Action<DaggerHandler>? = null) { daggerHandler.enable() action?.execute(daggerHandler) } }
  76. Set up the feature DSL // build-logic/src/main/kotlin/com/sample/features/FeatureHandler.kt @SampleCompanyExtensionMarker abstract class

    FeaturesHandler @Inject internal constructor(objects: ObjectFactory) { private val composeHandler = objects.newInstance<ComposeHandler>() private val daggerHandler = objects.newInstance<DaggerHandler>() internal fun applyTo(project: Project) { composeHandler.applyTo(project) daggerHandler.applyTo(project) } fun compose(action: Action<ComposeHandler>? = null) { composeHandler.enable() action?.execute(composeHandler) } fun dagger(action: Action<DaggerHandler>? = null) { daggerHandler.enable() action?.execute(daggerHandler) } }
  77. Compose UI feature // build-logic/src/main/kotlin/com/sample/features/ComposeHandler.kt @SampleCompanyExtensionMarker abstract class ComposeHandler @Inject

    internal constructor(objects: ObjectFactory) { private val enabled = objects.property<Boolean>().convention(false) internal var androidExtension: CommonExtension<*, *, *, *, *, *>? = null internal fun enable() { enabled.set(true) enabled.disallowChanges() androidExtension.apply { buildFeatures { compose = true } } } internal fun applyTo(project: Project) { if (enabled.get()) { project.plugins.apply("org.jetbrains.kotlin.plugin.compose") project.plugins.apply("org.jetbrains.compose") project.dependencies.apply { add("implementation", "org.jetbrains.compose.ui:ui:$composeVersion") add("implementation", "org.jetbrains.compose.runtime:runtime:$composeVersion") // ... } } } }
  78. Compose UI feature // build-logic/src/main/kotlin/com/sample/features/ComposeHandler.kt @SampleCompanyExtensionMarker abstract class ComposeHandler @Inject

    internal constructor(objects: ObjectFactory) { private val enabled = objects.property<Boolean>().convention(false) internal var androidExtension: CommonExtension<*, *, *, *, *, *>? = null internal fun enable() { enabled.set(true) enabled.disallowChanges() androidExtension.apply { buildFeatures { compose = true } } } internal fun applyTo(project: Project) { if (enabled.get()) { project.plugins.apply("org.jetbrains.kotlin.plugin.compose") project.plugins.apply("org.jetbrains.compose") project.dependencies.apply { add("implementation", "org.jetbrains.compose.ui:ui:$composeVersion") add("implementation", "org.jetbrains.compose.runtime:runtime:$composeVersion") // ... } } } }
  79. Providing a custom DSL • Declarative, discoverable • Hides the

    complexity away • Allows for easy migrations
  80. Changing Dagger KAPT to KSP // build-logic/src/main/kotlin/com/sample/features/DaggerHandler.kt abstract class DaggerHandler

    @Inject internal constructor(objects: ObjectFactory) { // ... private fun Project.setupDaggerCompiler() { if (useKsp.get()) { pluginManager.apply("com.google.devtools.ksp") dependencies.apply { add("ksp", daggerHiltCompiler) } } else { pluginManager.apply("kotlin-kapt") dependencies.apply { add("kapt", daggerHiltCompiler) } } } }