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.

Cf95f93e78f6d6dd0630049396f723c6?s=128

Xavier Gouchet

November 08, 2019
Tweet

Transcript

  1. Xavier F. Gouchet, Senior Software Engineer @xgouchet Rock the Gradle,

    Rule the world
  2. None
  3. INTRODUCTION 0 A (brief) Gradle introduction

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

    Dependency Management System High performance Build Management Task Dependency Graph
  5. 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
  6. BUILDSRC 1 “It’s a kind of Magic” — Queen

  7. ┬ MyProject ├┬ app/ │├── src/ │└── build.gradle ├┬ buildSrc/

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

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

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

    │├── src/ │├── build.gradle │└── settings.gradle ├─ build.gradle └─ settings.gradle Project Structure
  11. 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?
  12. Better dependency management Helper classes / methods Plugin configuration Custom

    plugin (Locally versioned with the project) What can we use it for?
  13. ⚠ “A single change in buildSrc causes the whole project

    to become out-of-date.”
  14. SCRIPTS 2 “Don’t repeat yourself” — Uncle Bob Martin

  15. DEPENDENCIES Write Once Use Everywhere https://unsplash.com/@eliabevces

  16. dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:…" implementation "androidx.appcompat:appcompat:…" implementation "androidx.core:core-ktx:…" implementation "androidx.annotation:annotation:…"

    } Dependencies app/build.gradle
  17. object Dependencies { object Versions { const val Kotlin =

    "1.3.50" const val AndroidX = "1.0.0" } // … } Dependencies buildSrc/src/main/kotlin/Dependencies.kt
  18. 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
  19. 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
  20. 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
  21. import Dependencies dependencies { implementation Dependencies.Libraries.Kotlin implementation Dependencies.Libraries.AndroidX } Dependencies

    app/build.gradle
  22. CONFIGURATIONS Hide the bulk https://unsplash.com/@adigold1

  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. import detektConfig detektConfig() Plugin Configuration app/build.gradle

  30. UTILITIES Expand your toolbox https://unsplash.com/@toddquackenbush

  31. android { defaultConfig { versionCode 31401 versionName "3.14.1" } }

    Version Code & Version Name app/build.gradle
  32. 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
  33. 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
  34. 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
  35. object App { val Version = Version(3, 14, 1) }

    Version Code & Version Name buildSrc/src/main/kotlin/App.kt
  36. import App android { defaultConfig { versionCode App.Version.code versionName App.Version.name

    } } Version Code & Version Name app/build.gradle
  37. PLUGINS 3 Now you’re thinking with Plugins

  38. Variants Graph What a Plugin can do… Tasks Extensions Per-module

    configurations Configuration Project Task Dependencies
  39. A concrete example Sharing localisation strings between platforms

  40. TASK The Core Feature https://unsplash.com/@wanxi

  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. A Custom Task import GetStringsTask task downloadStrings(type: GetStringsTask) { languages

    = ["en", "fr"] root = "$projectDir/src/main" baseUrl = "https://example.org/locales" } app/build.gradle
  47. EXTENSION Adding some flexibility https://unsplash.com/@glenncarstenspeters

  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. GOING FURTHER 4 “Don't stop me now (cause I’m having

    a good time!)” — Queen
  62. TASK GRAPH Where the Magic happen https://unsplash.com/@bill_oxford

  63. 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
  64. 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
  65. 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
  66. SIDENOTE ON TASK ORDER STRICT A.dependsOn(B) C.finalizedBy(B) LOOSE D.mustRunAfter(B) E.shouldRunAfter(B)

  67. SIDENOTE ON TASK ORDER LOOSE D.mustRunAfter(B) E.shouldRunAfter(B) $ gradle D

    > Task :app:D BUILD SUCCESSFUL in 4s 1 task executed
  68. 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
  69. SIDENOTE ON TASK ORDER LOOSE D.mustRunAfter(B) E.shouldRunAfter(B) $ gradle E

    > Task :app:E BUILD SUCCESSFUL in 15s 1 task executed
  70. 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
  71. 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
  72. PLUGIN INTEGRATION Help the developer use your Plugin https://unsplash.com/@momentsbygabriel

  73. Aliasing the Plugin apply plugin: "java-gradle-plugin" gradlePlugin { plugins {

    getStrings { id = "getStrings" // the alias implementationClass = "GetStringsPlugin" } } } buildSrc/build.gradle
  74. Aliasing the Plugin import GetStringsPlugin apply plugin: GetStringsPlugin app/build.gradle

  75. Aliasing the Plugin apply plugin: "getStrings" app/build.gradle

  76. Task’s group & description class GetStringsPlugin : Plugin<Project> { init

    { group = "mobileEra" description = "Downloads strings.xml from server." } // … } buildSrc/src/main/kotlin/GetStringsPlugin.kt
  77. 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
  78. 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
  79. GRADLE CACHE Avoid unnecessary work https://unsplash.com/@jasonrobertsphotography

  80. 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 } }
  81. 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") } } }
  82. It will never update until we change the config ?

  83. 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) } }
  84. INCREMENTAL TASKS Avoid unnecessary work https://unsplash.com/@pluyar

  85. 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 { // … } } }
  86. 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 { // … } } }
  87. 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 { // … } } }
  88. 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 { // … } } }
  89. 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 { // … } } }
  90. 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 { // … } } }
  91. TESTING YOUR PLUGIN #TestMatters Caspar Benson / Getty Images

  92. 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
  93. CREATING A DSL Simplify the configuration https://unsplash.com/@ratushny

  94. 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"] } } }
  95. DSL can go as deep as you need

  96. GENERATING STUFF Delegate the tedious tasks https://unsplash.com/@lennykuhne

  97. Generating Files Android Manifest Source code (JavaPoet / KotlinPoet) Resources

    (Strings, Drawables, Layouts, NavGraph, …) Assets…
  98. ⚠ “Make sure your generator task is executed at the

    right moment.”
  99. WRAPPING UP 5 You can enter here the subtitle if

    you need it
  100. buildSrc is always built and tested Take away A single

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

    maintainable and understandable as your production code.
  102. Get the slides at https://speakerdeck.com/xgouchet/rock-the-gradle-mobileera Check sample project at https://github.com/xgouchet/RockTheGradle/

  103. 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!