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

Rock the Gradle, Rule the world

Rock the Gradle, Rule the world

More often than not, an Android project's build.gradle files grow larger and more complex with time. This talk will look at the under-appreciated 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 F. Gouchet

April 24, 2018
Tweet

Video

More Decks by Xavier F. Gouchet

Other Decks in Programming

Transcript

  1. ABOUT… XAVIER F. GOUCHET LEAD ANDROID ENGINEER AT WORKWELL FLUENT

    IN ANDROID SINCE CUPCAKE ON ALL SOCIAL NETWORKS @XGOUCHET 2
  2. SHOW OF HANDS Configured project in gradle Write a custom

    task Write a custom plugin Followed Kotlinify your Gradle by Alexander Gherschon 4
  3. ┬ 📂 MyProject ├┬ 📂 app/ │├── 📁 src/ │└──

    📄 build.gradle ├┬ 📂 buildSrc/ │├── 📁 src/ │├── 📄 build.gradle ? │└── 📄 settings.gradle ├─ 📄 build.gradle └─ 📄 settings.gradle 8
  4. WHAT CAN IT DO FOR ME ? Better dependency management

    Helper classes / methods Custom tasks in dedicated classes Custom plugin Local a versionned with the project 9
  5. HOW DOES IT WORK ? Works like any module in

    your project Compiled and tested before any gradle task Groovy, Java, Kotlin, … 10
  6. STEP 2 : … THAT'S IT DEFAULT build.gradle apply plugin:

    'groovy' dependencies { compile gradleApi() compile localGroovy() } 13
  7. STEP 2 (BIS) : WITH KOTLIN buildscript { dependencies {

    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.30" } repositories { mavenCentral() } } apply plugin: 'kotlin' repositories { mavenCentral() google() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.2.30" compile "com.android.tools.build:gradle:3.1.1" } 14
  8. AFTER buildSrc/src/main/kotlin/Dependencies.kt object Dependencies { object Versions { const val

    Kotlin = "1.2.30" const val AndroidSupportLib = "27.1.1" } object Libraries { const val KotlinJre7 = "org.jetbrains.kotlin:kotlin-stdlib-jre7:${Versions.Kotlin}" @JvmField val SupportLibs = arrayOf("com.android.support:appcompat-v7:${Versions.AndroidSupportLib}", "com.android.support:design:${Versions.AndroidSupportLib}", "com.android.support:support-annotations:${Versions.AndroidSupportLib}") } } 17
  9. buildSrc/src/main/kotlin/Helper.kt object Helper { @JvmStatic @JvmOverloads fun ImThirsty(count: Int =

    42) { for (i in count downTo 0) { println("$i bottles of beer on the wall, \n" + "Take one down and pass it around…") } } } 20
  10. buildSrc/src/main/kotlin/DownloadStrings.kt open class DownloadStrings : DefaultTask() { var languages :

    Array<String> = arrayOf() var path = "" @TaskAction fun performTask() { for (l in languages) { Fuel.download("http://192.168.1.42/$l/strings.xml") .destination { _, _ -> File("$path/src/main/res/values-$l/strings.xml") } .response { _, res, _ -> println("$l → ${res.statusCode}") } } } } 23
  11. buildSrc/src/main/kotlin/RemotePlugin.kt class RemoteL10n : Plugin<Project> { override fun apply(project: Project)

    { val ext = project.extensions .create("remoteStrings", RemoteExt::class.java) val task = project.tasks .create("downloadStrings", DownloadStrings::class.java) .apply { projectBasePath = project.projectDir.path configuration = ext } } } 27
  12. app/build.gradle // […] import RemoteL10n apply plugin: RemoteL10n remoteStrings {

    languages = ["en", "fr"] remoteHost = "192.168.1.42" } 28
  13. SIDENOTE ON TASK MANAGEMENT class RemoteL10n : Plugin<Project> { override

    fun apply(project: Project) { // … project.afterEvaluate { p -> p.tasks .withType(GenerateResValues::class.java){ it.dependsOn(task) } } } } 29
  14. buildSrc/src/main/kotlin/RemoteExt.kt open class RemoteExt { var remoteHost: String = "127.0.0.1"

    var variants: NamedDomainObjectContainer<VariantL10n>? = null fun variants(configureClosure: Closure<VariantL10n>) { variants?.configure(configureClosure) } } open class VariantL10n(val name: String) { var languages: Array<String> = arrayOf() } 33
  15. buildSrc/src/main/kotlin/RemotePlugin.kt class RemoteL10n : Plugin<Project> { override fun apply(project: Project)

    { val ext = project.extensions .create("remoteStrings", RemoteExt::class.java) ext.variants = project.container(VariantL10n::class.java) // … } } 34
  16. app/build.gradle remoteL10n { remoteHost = "192.168.1.42" variants { debug {

    languages = ["en"] } freeRelease { languages = ["fr", "en"] } fullRelease { languages = ["fr", "en", "es", "de"] } } } 35
  17. class RemoteL10nTest { lateinit var fakeProject: Project @Before fun setUp()

    { fakeProject = ProjectBuilder.builder().build() fakeProject.plugins.apply(AppPlugin::class.java) fakeProject.plugins.apply(RemoteL10n::class.java) fakeProject.evaluate() // using a reflection UglyHack™ } } 37
  18. class RemoteL10nTest { // … @Test fun addsExtension() { val

    extension = fakeProject.extensions .getByName(RemoteL10n.EXTENSION_NAME) assert(extension is RemoteL10nExtension) } @Test fun addsTask() { val task = fakeProject.tasks.getByName(RemoteL10n.TASK_NAME) assert(task is DownloadStrings) } } 38
  19. UNIT TESTING THE TASK ITSELF Harder to do directly Delegate

    all business logic to a dedicated class 39
  20. buildSrc/src/main/kotlin/DownloadStrings.kt open class DownloadStrings : DefaultTask() { var projectBasePath =

    "." var configuration: RemoteExt = RemoteExt() @TaskAction fun performTask() { val fetcher = RemoteL10nFetcher() fetcher.fetchString( FuelDownloader(), projectBasePath, configuration) } } 42
  21. ALIASING THE PLUGIN buildSrc/build.gradle apply plugin: 'java-gradle-plugin' gradlePlugin { plugins

    { remoteL10n { id = "remoteL10n" // the important one - implementationClass = "com.packagename.RemoteL10n" } } } 45
  22. ADD TASK CATEGORY buildSrc/src/main/kotlin/DownloadStrings.kt open class DownloadStrings : DefaultTask() {

    init { group = "makers" description = "Downloads strings.xml from a remote server." } // … } 47
  23. gradle :tasks Android tasks ------------- androidDependencies - Displays the Android

    dependencies. signingReport - Displays the signing info for each variant. sourceSets - Prints out all the source sets defined in the project. … Makers tasks ------------ downloadRemoteStrings - Downloads strings.xml from a remote server. … 48
  24. DEFINING INPUTS / OUTPUTS @Input / @Output serializable objects @InputFile(s)

    / @OutputFile(s) @InputDirectory(ies) / @OutputDirectory(ies) @Nested / Console / @Internal / @Destroys / … @SkipWhenEmpty / @Optionnal 49
  25. buildSrc/src/main/kotlin/DownloadStrings.kt @Input fun getExtInputs(): List<String> { val inputs = mutableListOf<String>()

    configuration.variants?.forEach { v -> v.languages.forEach { l -> inputs.add("${v.name}/$l") } } return inputs } 50
  26. buildSrc/src/main/kotlin/DownloadStrings.kt @OutputFiles fun getTaskOOutputs(): List<File> { val outputs = mutableListOf<File>()

    configuration.variants?.forEach { v -> v.languages.forEach { l -> outputs.add( File("$projectBasePath/src/${v.name}/res/values-$l/strings.xml") ) } } return outputs } 51
  27. INCREMENTAL TASK @TaskAction fun incrementalAction(inputs: IncrementalTaskInputs) { if (inputs.isIncremental) {

    inputs.outOfDate { println("Out of date: ${it.file.name}") } inputs.removed { println("Removed: ${it.file.name}") } } else { // … } } 55
  28. WHAT ELSE ? Generate source code (JavaPoet / KotlinPoet /

    … ) Generate resources (drawables, layouts, …) Custom lint / analysis / … 56
  29. REMINDERS Tasks & Extensions classes must be open DSL can

    go as deep as you need buildSrc is always built and tested Use apply from: "$project.rootDir/config/findbugs.gradle" Your build code should be as clean as your production code 58
  30. THANKS FOR YOUR ATTENTION ANY QUESTION ? Sample app :

    github.com/xgouchet/RockTheGradle 59