Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Rock The Gradle MobileEra

Rock The Gradle MobileEra

More often than none, an Android project's build.gradle files grow larger and more complex with time. This talk will look at the underappreciated buildSrc folder, which allows you to create plugins directly from your project, but also can help you make your build.gradle files cleaner and more readable.

Xavier Gouchet

November 08, 2019
Tweet

More Decks by Xavier Gouchet

Other Decks in Programming

Transcript

  1. WHAT IS GRADLE ? General Purpose Language Agnostic Feature Agnostic

    Dependency Management System High performance Build Management Task Dependency Graph
  2. THE GRADLE ALGORITHM INIT Launches the JVM Analyse the working

    directory Creates the Project object(s) Compiles the buildSrc module(s) CONFIG EXEC Executes all the build.gradle scripts Creates & configures tasks Resolve dependencies Execute the relevant tasks
  3. ┬ MyProject ├┬ app/ │├── src/ │└── build.gradle ├┬ buildSrc/

    │├── src/ │├── build.gradle │└── settings.gradle ├─ build.gradle └─ settings.gradle Project Structure
  4. ┬ MyProject ├┬ app/ │├── src/ │└── build.gradle ├┬ buildSrc/

    │├── src/ │├── build.gradle │└── settings.gradle ├─ build.gradle └─ settings.gradle Project Structure
  5. ┬ MyProject ├┬ app/ │├── src/ │└── build.gradle ├┬ buildSrc/

    │├── src/ │├── build.gradle │└── settings.gradle ├─ build.gradle └─ settings.gradle Project Structure
  6. ┬ MyProject ├┬ app/ │├── src/ │└── build.gradle ├┬ buildSrc/

    │├── src/ │├── build.gradle │└── settings.gradle ├─ build.gradle └─ settings.gradle Project Structure
  7. Works like any module in your project Compiled and tested

    before any gradle task Groovy, Java, Kotlin, … Any public class / method becomes available in gradle scripts How does it work?
  8. Better dependency management Helper classes / methods Plugin configuration Custom

    plugin (Locally versioned with the project) What can we use it for?
  9. object Dependencies { object Versions { const val Kotlin =

    "1.3.50" const val AndroidX = "1.0.0" } // … } Dependencies buildSrc/src/main/kotlin/Dependencies.kt
  10. object Dependencies { // … object Libraries { const val

    Kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.Kotlin}" @JvmField val AndroidX = arrayOf( "androidx.appcompat:appcompat:${Versions.AndroidX}", "androidx.core:core-ktx:${Versions.AndroidX}", "androidx.annotation:annotation:${Versions.AndroidX}" ) } } Dependencies buildSrc/src/main/kotlin/Dependencies.kt
  11. object Dependencies { // … object Libraries { const val

    Kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.Kotlin}" @JvmField val AndroidX = arrayOf( "androidx.appcompat:appcompat:${Versions.AndroidX}", "androidx.core:core-ktx:${Versions.AndroidX}", "androidx.annotation:annotation:${Versions.AndroidX}" ) } } Dependencies buildSrc/src/main/kotlin/Dependencies.kt
  12. object Dependencies { // … object Libraries { const val

    Kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.Kotlin}" @JvmField val AndroidX = arrayOf( "androidx.appcompat:appcompat:${Versions.AndroidX}", "androidx.core:core-ktx:${Versions.AndroidX}", "androidx.annotation:annotation:${Versions.AndroidX}" ) } } Dependencies buildSrc/src/main/kotlin/Dependencies.kt
  13. detekt { toolVersion = "1.0.1" input = files("$projectDir/src/main/java") config =

    files("$project.rootDir/config/detekt.yml") reports.xml { enabled = true destination = file("build/reports/detekt.xml") } } check.dependsOn("detekt") Plugin Configuration app/build.gradle
  14. fun Project.detektConfig() { val ext = extensions.findByType(DetektExtension::class) ext.version = "1.0.1"

    ext.input = files("$projectDir/src/main/kotlin") // … tasks.named("check") { dependsOn("detekt") } } Plugin Configuration buildSrc/src/main/kotlin/DetektConfig.kt
  15. fun Project.detektConfig() { val ext = extensions.findByType(DetektExtension::class) ext.version = "1.0.1"

    ext.input = files("$projectDir/src/main/kotlin") // … tasks.named("check") { dependsOn("detekt") } } Plugin Configuration buildSrc/src/main/kotlin/DetektConfig.kt
  16. fun Project.detektConfig() { val ext = extensions.findByType(DetektExtension::class) ext.version = "1.0.1"

    ext.input = files("$projectDir/src/main/kotlin") // … tasks.named("check") { dependsOn("detekt") } } Plugin Configuration buildSrc/src/main/kotlin/DetektConfig.kt
  17. fun Project.detektConfig() { val ext = extensions.findByType(DetektExtension::class) ext.version = "1.0.1"

    ext.input = files("$projectDir/src/main/kotlin") // … tasks.named("check") { dependsOn("detekt") } } Plugin Configuration buildSrc/src/main/kotlin/DetektConfig.kt
  18. fun Project.detektConfig() { val ext = extensions.findByType(DetektExtension::class) ext.version = "1.0.1"

    ext.input = files("$projectDir/src/main/kotlin") // … tasks.named("check") { dependsOn("detekt") } } Plugin Configuration buildSrc/src/main/kotlin/DetektConfig.kt
  19. android { defaultConfig { versionCode 31401 versionName "3.14.1" } }

    Version Code & Version Name app/build.gradle
  20. data class Version( val major: Int, val minor: Int, val

    hotfix: Int ) { val name = "$major.$minor.$hotfix" val code = (major × 10000) + (minor × 100) + hotfix } Version Code & Version Name buildSrc/src/main/kotlin/Version.kt
  21. data class Version( val major: Int, val minor: Int, val

    hotfix: Int ) { val name = "$major.$minor.$hotfix" val code = (major × 10000) + (minor × 100) + hotfix } Version Code & Version Name buildSrc/src/main/kotlin/Version.kt
  22. data class Version( val major: Int, val minor: Int, val

    hotfix: Int ) { val name = "$major.$minor.$hotfix" val code = (major × 10000) + (minor × 100) + hotfix } Version Code & Version Name buildSrc/src/main/kotlin/Version.kt
  23. object App { val Version = Version(3, 14, 1) }

    Version Code & Version Name buildSrc/src/main/kotlin/App.kt
  24. Variants Graph What a Plugin can do… Tasks Extensions Per-module

    configurations Configuration Project Task Dependencies
  25. A Custom Task open class GetStringsTask : DefaultTask() { var

    languages : Array<String> = arrayOf() var root = "" var baseUrl = "http://127.0.0.1" @TaskAction fun performTask() { for (l in languages) { Downloader.download("$baseUrl/$l/strings.xml", "$root/res/values-$l/strings.xml") } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  26. A Custom Task open class GetStringsTask : DefaultTask() { var

    languages : Array<String> = arrayOf() var root = "" var baseUrl = "http://127.0.0.1" @TaskAction fun performTask() { for (l in languages) { Downloader.download("$baseUrl/$l/strings.xml", "$root/res/values-$l/strings.xml") } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  27. A Custom Task open class GetStringsTask : DefaultTask() { var

    languages : Array<String> = arrayOf() var root = "" var baseUrl = "http://127.0.0.1" @TaskAction fun performTask() { for (l in languages) { Downloader.download("$baseUrl/$l/strings.xml", "$root/res/values-$l/strings.xml") } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  28. A Custom Task open class GetStringsTask : DefaultTask() { var

    languages : Array<String> = arrayOf() var root = "" var baseUrl = "http://127.0.0.1" @TaskAction fun performTask() { for (l in languages) { Downloader.download("$baseUrl/$l/strings.xml", "$root/res/values-$l/strings.xml") } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  29. A Custom Task open class GetStringsTask : DefaultTask() { var

    languages : Array<String> = arrayOf() var root = "" var baseUrl = "http://127.0.0.1" @TaskAction fun performTask() { for (l in languages) { Downloader.download("$baseUrl/$l/strings.xml", "$root/res/values-$l/strings.xml") } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  30. A Custom Task import GetStringsTask task downloadStrings(type: GetStringsTask) { languages

    = ["en", "fr"] root = "$projectDir/src/main" baseUrl = "https://example.org/locales" } app/build.gradle
  31. A Custom Extension open class GetStringsExt( var languages: Array<String> =

    arrayOf(), var baseUrl: String = "http://127.0.0.1" ) buildSrc/src/main/kotlin/GetStringsExt.kt
  32. A Custom Extension open class GetStringsTask : DefaultTask() { var

    root = "" var extension = GetStringsExt() @TaskAction fun performTask() { for (l in extension.languages) { val url = "${extension.baseUrl}/$l/strings.xml" val path = "$root/res/values-$l/strings.xml" Downloader.download(url, path) } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  33. A Custom Extension open class GetStringsTask : DefaultTask() { var

    root = "" var extension = GetStringsExt() @TaskAction fun performTask() { for (l in extension.languages) { val url = "${extension.baseUrl}/$l/strings.xml" val path = "$root/res/values-$l/strings.xml" Downloader.download(url, path) } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  34. A Custom Extension open class GetStringsTask : DefaultTask() { var

    root = "" var extension = GetStringsExt() @TaskAction fun performTask() { for (l in extension.languages) { val url = "${extension.baseUrl}/$l/strings.xml" val path = "$root/res/values-$l/strings.xml" Downloader.download(url, path) } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  35. A Custom Extension open class GetStringsTask : DefaultTask() { var

    root = "" var extension = GetStringsExt() @TaskAction fun performTask() { for (l in extension.languages) { val url = "${extension.baseUrl}/$l/strings.xml" val path = "$root/res/values-$l/strings.xml" Downloader.download(url, path) } } } buildSrc/src/main/kotlin/GetStringsTask.kt
  36. A Custom Extension… and a Plugin class GetStringsPlugin : Plugin<Project>

    { override fun apply(project: Project) { val ext = project.extensions .create("getStrings", GetStringsExt::class.java) val task = project.tasks .create("getStrings", GetStringsTask::class.java) task.apply { root = "${project.projectDir.path}/src/main" extension = ext } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  37. A Custom Extension… and a Plugin class GetStringsPlugin : Plugin<Project>

    { override fun apply(project: Project) { val ext = project.extensions .create("getStrings", GetStringsExt::class.java) val task = project.tasks .create("getStrings", GetStringsTask::class.java) task.apply { root = "${project.projectDir.path}/src/main" extension = ext } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  38. A Custom Extension… and a Plugin class GetStringsPlugin : Plugin<Project>

    { override fun apply(project: Project) { val ext = project.extensions .create("getStrings", GetStringsExt::class.java) val task = project.tasks .create("getStrings", GetStringsTask::class.java) task.apply { root = "${project.projectDir.path}/src/main" extension = ext } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  39. A Custom Extension… and a Plugin class GetStringsPlugin : Plugin<Project>

    { override fun apply(project: Project) { val ext = project.extensions .create("getStrings", GetStringsExt::class.java) val task = project.tasks .create("getStrings", GetStringsTask::class.java) task.apply { root = "${project.projectDir.path}/src/main" extension = ext } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  40. A Custom Extension… and a Plugin class GetStringsPlugin : Plugin<Project>

    { override fun apply(project: Project) { val ext = project.extensions .create("getStrings", GetStringsExt::class.java) val task = project.tasks .create("getStrings", GetStringsTask::class.java) task.apply { root = "${project.projectDir.path}/src/main" extension = ext } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  41. A Custom Extension… and a Plugin class GetStringsPlugin : Plugin<Project>

    { override fun apply(project: Project) { val ext = project.extensions .create("getStrings", GetStringsExt::class.java) val task = project.tasks .create("getStrings", GetStringsTask::class.java) task.apply { root = "${project.projectDir.path}/src/main" extension = ext } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  42. A Custom Extension… and a Plugin import GetStringsPlugin apply plugin:

    GetStringsPlugin getStrings { languages = ["en", "fr"] baseUrl = "https://example.org/locales" // no need to specify the root path anymore } app/build.gradle
  43. The extension makes it easy to configure The plugin can

    fill in properties automatically The plugin can generate tasks per variant The plugin can manipulate the graph Why use a Plugin+Extension
  44. Ordering Tasks class GetStringsPlugin : Plugin<Project> { override fun apply(project:

    Project) { // … project.afterEvaluate { p -> p.tasks .withType(GenerateResValues::class.java) { it.dependsOn(task) } } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  45. Ordering Tasks class GetStringsPlugin : Plugin<Project> { override fun apply(project:

    Project) { // … project.afterEvaluate { p -> p.tasks .withType(GenerateResValues::class.java) { it.dependsOn(task) } } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  46. Ordering Tasks class GetStringsPlugin : Plugin<Project> { override fun apply(project:

    Project) { // … project.afterEvaluate { p -> p.tasks .withType(GenerateResValues::class.java) { it.dependsOn(task) } } } } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  47. SIDENOTE ON TASK ORDER LOOSE D.mustRunAfter(B) E.shouldRunAfter(B) $ gradle D

    > Task :app:D BUILD SUCCESSFUL in 4s 1 task executed
  48. SIDENOTE ON TASK ORDER LOOSE D.mustRunAfter(B) E.shouldRunAfter(B) $ gradle D

    B > Task :app:B > Task :app:D BUILD SUCCESSFUL in 8s 2 tasks executed
  49. SIDENOTE ON TASK ORDER LOOSE D.mustRunAfter(B) E.shouldRunAfter(B) $ gradle E

    > Task :app:E BUILD SUCCESSFUL in 15s 1 task executed
  50. SIDENOTE ON TASK ORDER LOOSE D.mustRunAfter(B) E.shouldRunAfter(B) $ gradle E

    B > Task :app:B > Task :app:E BUILD SUCCESSFUL in 16s 2 tasks executed
  51. SIDENOTE ON TASK ORDER LOOSE D.mustRunAfter(B) E.shouldRunAfter(B) B.dependsOn(A) A.dependsOn(E) $

    gradle E B > Task :app:E > Task :app:A > Task :app:B BUILD SUCCESSFUL in 23s 3 tasks executed
  52. Aliasing the Plugin apply plugin: "java-gradle-plugin" gradlePlugin { plugins {

    getStrings { id = "getStrings" // the alias implementationClass = "GetStringsPlugin" } } } buildSrc/build.gradle
  53. Task’s group & description class GetStringsPlugin : Plugin<Project> { init

    { group = "mobileEra" description = "Downloads strings.xml from server." } // … } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  54. Android tasks ------------- androidDependencies - Displays the Android dependencies. sourceSets

    - Prints out all the source sets in the project. MobileEra tasks --------------- getStrings - Downloads strings.xml from a server. Verification tasks ------------------ lint - Runs lint on all variants. Task’s group & description $ gradlew :app:tasks
  55. Android tasks ------------- androidDependencies - Displays the Android dependencies. sourceSets

    - Prints out all the source sets in the project. MobileEra tasks --------------- getStrings - Downloads strings.xml from a server. Verification tasks ------------------ lint - Runs lint on all variants. Task’s group & description $ gradlew :app:tasks
  56. buildSrc/src/main/kotlin/GetStringsTask.kt Defining Inputs & Outputs open class GetStringsTask : DefaultTask()

    { @Input fun getLanguagesInputs(): List<String> { return extension.languages.toList() } @Input fun getBaseUrlInput(): String { return extension.baseUrl } }
  57. buildSrc/src/main/kotlin/GetStringsTask.kt Defining Inputs & Outputs open class GetStringsTask : DefaultTask()

    { @OutputFiles fun getTaskOutputs(): List<File> { return extension.languages.map { l -> File("$root/res/values-$l/strings.xml") } } }
  58. buildSrc/src/main/kotlin/GetStringsTask.kt Defining Inputs & Outputs open class GetStringsTask : DefaultTask()

    { @Input fun getDateInput() : String { val formatter = DateTimeFormatter.ISO_DATE return LocalDateTime.now().format(formatter) } }
  59. buildSrc/src/main/kotlin/GetStringsTask.kt Incremental Task open class GetStringsTask : DefaultTask() { @TaskAction

    fun incrementAction(inputs: IncrementalTaskInputs) { if (inputs.isIncremental) { inputs.outOfDate { println("Out of date: $it") } inputs.removed { println("Removed: $it") } } else { // … } } }
  60. buildSrc/src/main/kotlin/GetStringsTask.kt Incremental Task open class GetStringsTask : DefaultTask() { @TaskAction

    fun incrementAction(inputs: IncrementalTaskInputs) { if (inputs.isIncremental) { inputs.outOfDate { println("Out of date: $it") } inputs.removed { println("Removed: $it") } } else { // … } } }
  61. buildSrc/src/main/kotlin/GetStringsTask.kt Incremental Task open class GetStringsTask : DefaultTask() { @TaskAction

    fun incrementAction(inputs: IncrementalTaskInputs) { if (inputs.isIncremental) { inputs.outOfDate { println("Out of date: $it") } inputs.removed { println("Removed: $it") } } else { // … } } }
  62. buildSrc/src/main/kotlin/GetStringsTask.kt Incremental Task open class GetStringsTask : DefaultTask() { @TaskAction

    fun incrementAction(inputs: IncrementalTaskInputs) { if (inputs.isIncremental) { inputs.outOfDate { println("Out of date: $it") } inputs.removed { println("Removed: $it") } } else { // … } } }
  63. buildSrc/src/main/kotlin/GetStringsTask.kt Incremental Task open class GetStringsTask : DefaultTask() { @TaskAction

    fun incrementAction(inputs: IncrementalTaskInputs) { if (inputs.isIncremental) { inputs.outOfDate { println("Out of date: $it") } inputs.removed { println("Removed: $it") } } else { // … } } }
  64. buildSrc/src/main/kotlin/GetStringsTask.kt Incremental Task open class GetStringsTask : DefaultTask() { @TaskAction

    fun incrementAction(inputs: IncrementalTaskInputs) { if (inputs.isIncremental) { inputs.outOfDate { println("Out of date: $it") } inputs.removed { println("Removed: $it") } } else { // … } } }
  65. PLUGIN Only test the action of the Plugin itself INTEGRATION

    Dummy project to test real life scenarios Testing a Gradle Plugin buildSrc is tested on each build! TASK Delegate to other classes as much as possible
  66. app/build.gradle Creating a DSL getStrings { baseUrl = "https://example.org/locales" variants

    { debug { languages = ["en"] } free { languages = ["en", "fr"] } fullRelease { languages = ["en", "fr", "es"] } } }
  67. Generating Files Android Manifest Source code (JavaPoet / KotlinPoet) Resources

    (Strings, Drawables, Layouts, NavGraph, …) Assets…
  68. buildSrc is always built and tested Take away A single

    change in buildSrc invalidates all tasks Publish your plugins
  69. Your build scripts are still code. Keep them as clean,

    maintainable and understandable as your production code.
  70. CREDITS: This presentation template was created by Slidesgo, including icons

    by Flaticon, and infographics & images by Freepik. Do you have any questions? @xgouchet @datadoghq THANK YOU!