Slide 1

Slide 1 text

Scaling your build logic Antal Monori, Android Engineer @ Wise

Slide 2

Slide 2 text

Show your hands If you know this little elephant...

Slide 3

Slide 3 text

Disclaimer1 1 This presentation will focus on Gradle as a build tool.

Slide 4

Slide 4 text

Definition build logic (n.)

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Goals

Slide 8

Slide 8 text

Goals 1. Some Gradle fundamentals

Slide 9

Slide 9 text

Goals 1. Some Gradle fundamentals 2. How to create a Gradle plugin

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Let's get started // settings.gradle.kts rootProject.name="sample-gradle-project" pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } include (":app")

Slide 14

Slide 14 text

Let's get started // settings.gradle.kts rootProject.name="sample-gradle-project" pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } include (":app")

Slide 15

Slide 15 text

Disclaimer3 3 Always consider the stage of the project. Just as to any approach there are always caveats.

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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") // ...

Slide 19

Slide 19 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 20

Slide 20 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 21

Slide 21 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 22

Slide 22 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 23

Slide 23 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 24

Slide 24 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 25

Slide 25 text

Growing pains This is when the real growing pains start to show their face.

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 31

Slide 31 text

Let's start searching for advice

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

Applying what the internet says // build.gradle.kts subprojects { android { // ERROR: Unresolved reference 'android'. compileSdk = 33 defaultConfig { minSdk = 24 } } }

Slide 34

Slide 34 text

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("android") { compileSdkVersion(33) defaultConfig { minSdk = 24 } } }

Slide 35

Slide 35 text

Tip #1 Avoid using allprojects {} and subprojects {} because:

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Tip #1 Avoid using allprojects {} and subprojects {} because: • Breaks isolation • Performance hit • Hard to debug Instead Use pre-compiled scripts or convention plugins

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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.

Slide 42

Slide 42 text

They are the same picture.

Slide 43

Slide 43 text

Are they the same? Both pre-compiled scripts and convention plugins:

Slide 44

Slide 44 text

Are they the same? Both pre-compiled scripts and convention plugins: • Written in Groovy or Kotlin DSL

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Let's start with pre-compiled scripts

Slide 48

Slide 48 text

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() }

Slide 49

Slide 49 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 50

Slide 50 text

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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies { /* ... */ }

Slide 51

Slide 51 text

The generated code // buildSrc/build/generated-sources/kotlin-dsl-plugins/kotlin/Sample_android_libraryPlugin.kt public class Sample_android_libraryPlugin : org.gradle.api.Plugin { 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 } } }

Slide 52

Slide 52 text

Applying the plugin // layer_1/module_1_20/build.gradle.kts plugins { id("sample.android.library") } dependencies { /* ... */ }

Slide 53

Slide 53 text

Successfully extracted build logic Now we just need to apply to all build scripts.

Slide 54

Slide 54 text

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 }

Slide 55

Slide 55 text

sample.root.gradle.kts plugins { }

Slide 56

Slide 56 text

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() }

Slide 57

Slide 57 text

Applying the plugin // build.gradle.kts plugins { id("sample.android.root") }

Slide 58

Slide 58 text

✅ All good, right?

Slide 59

Slide 59 text

Tip #2 Avoid using buildSrc:

Slide 60

Slide 60 text

Tip #2 Avoid using buildSrc: • It's a special directory, only one can exist

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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") // ...

Slide 65

Slide 65 text

✅ Sync and ready to go

Slide 66

Slide 66 text

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() }

Slide 67

Slide 67 text

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() }

Slide 68

Slide 68 text

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 { /* .. */ }

Slide 69

Slide 69 text

Migrating to convention plugins // build-logic/src/main/kotlin/convention/SampleRootPlugin.kt internal class SampleRootPlugin : Plugin { override fun apply(project: Project) { require(project == project.rootProject) { "Plugin must be applied to root project!" } } }

Slide 70

Slide 70 text

Migrating to convention plugins internal class SampleAndroidApplicationPlugin : Plugin { 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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies.apply { add("implementation", "androidx.appcompat:appcompat:1.6.1") // ... } } } }

Slide 71

Slide 71 text

Migrating to convention plugins internal class SampleAndroidApplicationPlugin : Plugin { 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().configureEach { kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } } dependencies.apply { add("implementation", "androidx.appcompat:appcompat:1.6.1") // ... } } } }

Slide 72

Slide 72 text

Migrating to convention plugins // app/build.gradle.kts plugins { id("sample.android.application") } dependencies { /* ... */ }

Slide 73

Slide 73 text

Done

Slide 74

Slide 74 text

Achievements, so far...

Slide 75

Slide 75 text

Achievements, so far... • We went from 1000s of lines to 100s of lines of code

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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)

Slide 78

Slide 78 text

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") // ...

Slide 79

Slide 79 text

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 } }

Slide 80

Slide 80 text

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 } }

Slide 81

Slide 81 text

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 } }

Slide 82

Slide 82 text

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 } }

Slide 83

Slide 83 text

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() }

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

Creating a settings convention plugin // settings.gradle.kts rootProject.name="sample-gradle-project" pluginManagement { includeBuild("build-logic-settings") } plugins { id("sample.settings") }

Slide 88

Slide 88 text

✅ Pretty much done

Slide 89

Slide 89 text

Tip #3 Android Grade Plugin 8.0.010 now ships with a settings plugin artifact: plugins { id("com.android.settings") 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

Slide 90

Slide 90 text

Apply directly // settings.gradle.kts rootProject.name="sample-gradle-project" pluginManagement { includeBuild("build-logic-settings") } plugins { id("sample.settings") id("com.android.settings") version "8.7.2" }

Slide 91

Slide 91 text

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() }

Slide 92

Slide 92 text

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 { override fun apply(settings: Settings) { with(settings) { // ... pluginManager.apply(SettingsPlugin::class.java) extensions.getByType().apply { minSdk = 24 compileSdk = 33 } } } }

Slide 93

Slide 93 text

Recap We now have:

Slide 94

Slide 94 text

Recap We now have: 1. Understand how to structure build logic

Slide 95

Slide 95 text

Recap We now have: 1. Understand how to structure build logic 2. Creating and applying convention plugins

Slide 96

Slide 96 text

Not so fast

Slide 97

Slide 97 text

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") }

Slide 98

Slide 98 text

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 }

Slide 99

Slide 99 text

What if... // layer_1/module_1_20/build.gradle.kts plugins { id("sample.android.library") } sampleCompany { features { compose() dagger() } }

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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) { action.execute(featuresHandler) } internal fun applyTo(project: Project) { project.afterEvaluate { featuresHandler.applyTo(project) } } }

Slide 109

Slide 109 text

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) { action.execute(featuresHandler) } internal fun applyTo(project: Project) { project.afterEvaluate { featuresHandler.applyTo(project) } } }

Slide 110

Slide 110 text

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) { action.execute(featuresHandler) } internal fun applyTo(project: Project) { project.afterEvaluate { featuresHandler.applyTo(project) } } }

Slide 111

Slide 111 text

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() private val daggerHandler = objects.newInstance() internal fun applyTo(project: Project) { composeHandler.applyTo(project) daggerHandler.applyTo(project) } fun compose(action: Action? = null) { composeHandler.enable() action?.execute(composeHandler) } fun dagger(action: Action? = null) { daggerHandler.enable() action?.execute(daggerHandler) } }

Slide 112

Slide 112 text

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() private val daggerHandler = objects.newInstance() internal fun applyTo(project: Project) { composeHandler.applyTo(project) daggerHandler.applyTo(project) } fun compose(action: Action? = null) { composeHandler.enable() action?.execute(composeHandler) } fun dagger(action: Action? = null) { daggerHandler.enable() action?.execute(daggerHandler) } }

Slide 113

Slide 113 text

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().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") // ... } } } }

Slide 114

Slide 114 text

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().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") // ... } } } }

Slide 115

Slide 115 text

Providing a custom DSL

Slide 116

Slide 116 text

Providing a custom DSL • Declarative, discoverable

Slide 117

Slide 117 text

Providing a custom DSL • Declarative, discoverable • Hides the complexity away

Slide 118

Slide 118 text

Providing a custom DSL • Declarative, discoverable • Hides the complexity away • Allows for easy migrations

Slide 119

Slide 119 text

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) } } } }

Slide 120

Slide 120 text

How about discoverability

Slide 121

Slide 121 text

No content

Slide 122

Slide 122 text

Acknowledgements

Slide 123

Slide 123 text

Herding Elephants 13 13 https://developer.squareup.com/blog/herding-elephants/

Slide 124

Slide 124 text

NowInAndroid 14 14 https://github.com/android/nowinandroid

Slide 125

Slide 125 text

slackhq/foundry 15 15 https://github.com/slackhq/foundry

Slide 126

Slide 126 text

Find me on the Fediverse androiddev.social/@antal

Slide 127

Slide 127 text

Thank you, question?