Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

INTRODUCTION 0 A (brief) Gradle introduction

Slide 4

Slide 4 text

WHAT IS GRADLE ? General Purpose Language Agnostic Feature Agnostic Dependency Management System High performance Build Management Task Dependency Graph

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

BUILDSRC 1 “It’s a kind of Magic” — Queen

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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?

Slide 12

Slide 12 text

Better dependency management Helper classes / methods Plugin configuration Custom plugin (Locally versioned with the project) What can we use it for?

Slide 13

Slide 13 text

⚠ “A single change in buildSrc causes the whole project to become out-of-date.”

Slide 14

Slide 14 text

SCRIPTS 2 “Don’t repeat yourself” — Uncle Bob Martin

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:…" implementation "androidx.appcompat:appcompat:…" implementation "androidx.core:core-ktx:…" implementation "androidx.annotation:annotation:…" } Dependencies app/build.gradle

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

import Dependencies dependencies { implementation Dependencies.Libraries.Kotlin implementation Dependencies.Libraries.AndroidX } Dependencies app/build.gradle

Slide 22

Slide 22 text

CONFIGURATIONS Hide the bulk https://unsplash.com/@adigold1

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

import detektConfig detektConfig() Plugin Configuration app/build.gradle

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

android { defaultConfig { versionCode 31401 versionName "3.14.1" } } Version Code & Version Name app/build.gradle

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

object App { val Version = Version(3, 14, 1) } Version Code & Version Name buildSrc/src/main/kotlin/App.kt

Slide 36

Slide 36 text

import App android { defaultConfig { versionCode App.Version.code versionName App.Version.name } } Version Code & Version Name app/build.gradle

Slide 37

Slide 37 text

PLUGINS 3 Now you’re thinking with Plugins

Slide 38

Slide 38 text

Variants Graph What a Plugin can do… Tasks Extensions Per-module configurations Configuration Project Task Dependencies

Slide 39

Slide 39 text

A concrete example Sharing localisation strings between platforms

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

A Custom Task open class GetStringsTask : DefaultTask() { var languages : Array = 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

Slide 42

Slide 42 text

A Custom Task open class GetStringsTask : DefaultTask() { var languages : Array = 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

Slide 43

Slide 43 text

A Custom Task open class GetStringsTask : DefaultTask() { var languages : Array = 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

Slide 44

Slide 44 text

A Custom Task open class GetStringsTask : DefaultTask() { var languages : Array = 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

Slide 45

Slide 45 text

A Custom Task open class GetStringsTask : DefaultTask() { var languages : Array = 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

Slide 46

Slide 46 text

A Custom Task import GetStringsTask task downloadStrings(type: GetStringsTask) { languages = ["en", "fr"] root = "$projectDir/src/main" baseUrl = "https://example.org/locales" } app/build.gradle

Slide 47

Slide 47 text

EXTENSION Adding some flexibility https://unsplash.com/@glenncarstenspeters

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

A Custom Extension… and a Plugin class GetStringsPlugin : Plugin { 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

Slide 54

Slide 54 text

A Custom Extension… and a Plugin class GetStringsPlugin : Plugin { 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

Slide 55

Slide 55 text

A Custom Extension… and a Plugin class GetStringsPlugin : Plugin { 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

Slide 56

Slide 56 text

A Custom Extension… and a Plugin class GetStringsPlugin : Plugin { 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

Slide 57

Slide 57 text

A Custom Extension… and a Plugin class GetStringsPlugin : Plugin { 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

Slide 58

Slide 58 text

A Custom Extension… and a Plugin class GetStringsPlugin : Plugin { 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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

GOING FURTHER 4 “Don't stop me now (cause I’m having a good time!)” — Queen

Slide 62

Slide 62 text

TASK GRAPH Where the Magic happen https://unsplash.com/@bill_oxford

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

SIDENOTE ON TASK ORDER STRICT A.dependsOn(B) C.finalizedBy(B) LOOSE D.mustRunAfter(B) E.shouldRunAfter(B)

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

PLUGIN INTEGRATION Help the developer use your Plugin https://unsplash.com/@momentsbygabriel

Slide 73

Slide 73 text

Aliasing the Plugin apply plugin: "java-gradle-plugin" gradlePlugin { plugins { getStrings { id = "getStrings" // the alias implementationClass = "GetStringsPlugin" } } } buildSrc/build.gradle

Slide 74

Slide 74 text

Aliasing the Plugin import GetStringsPlugin apply plugin: GetStringsPlugin app/build.gradle

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

Task’s group & description class GetStringsPlugin : Plugin { init { group = "mobileEra" description = "Downloads strings.xml from server." } // … } buildSrc/src/main/kotlin/GetStringsPlugin.kt

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

GRADLE CACHE Avoid unnecessary work https://unsplash.com/@jasonrobertsphotography

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

It will never update until we change the config ?

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

INCREMENTAL TASKS Avoid unnecessary work https://unsplash.com/@pluyar

Slide 85

Slide 85 text

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 { // … } } }

Slide 86

Slide 86 text

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 { // … } } }

Slide 87

Slide 87 text

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 { // … } } }

Slide 88

Slide 88 text

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 { // … } } }

Slide 89

Slide 89 text

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 { // … } } }

Slide 90

Slide 90 text

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 { // … } } }

Slide 91

Slide 91 text

TESTING YOUR PLUGIN #TestMatters Caspar Benson / Getty Images

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

CREATING A DSL Simplify the configuration https://unsplash.com/@ratushny

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

DSL can go as deep as you need

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

Generating Files Android Manifest Source code (JavaPoet / KotlinPoet) Resources (Strings, Drawables, Layouts, NavGraph, …) Assets…

Slide 98

Slide 98 text

⚠ “Make sure your generator task is executed at the right moment.”

Slide 99

Slide 99 text

WRAPPING UP 5 You can enter here the subtitle if you need it

Slide 100

Slide 100 text

buildSrc is always built and tested Take away A single change in buildSrc invalidates all tasks Publish your plugins

Slide 101

Slide 101 text

Your build scripts are still code. Keep them as clean, maintainable and understandable as your production code.

Slide 102

Slide 102 text

Get the slides at https://speakerdeck.com/xgouchet/rock-the-gradle-mobileera Check sample project at https://github.com/xgouchet/RockTheGradle/

Slide 103

Slide 103 text

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!