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

Gradle plugins, take it to the next level

Gradle plugins, take it to the next level

It's now a long time Gradle is the official build system for Android. And as a very good developer you already switched to it, and you customize it depending on your needs. Most of the time, the cleaner way to manage all these customizations is to build Gradle plugins. During this talk you will discover best practices about building your plugin to make it a good citizen, more efficient, and more famous!
This presentation will tell about:
- Building a Gradly DSL
- Interact with the Android Gradle Plugin
- Test your project on the good way

Eyal LEZMY

October 30, 2015
Tweet

More Decks by Eyal LEZMY

Other Decks in Programming

Transcript

  1. Build scripts Your build.gradle file Script plugins The customization you

    start writing Binary plugins The code I want you to write BASICS Gradle Plugins Types
  2. Is a piece of work for a build Compiling a

    class, generating javadoc, ... Can be manipulated doFirst, doLast Can inherits from another type Can depend on another task dependsOn, finalizedBy BASICS The Gradle Task
  3. Is a piece of work for a build Compiling a

    class, generating javadoc, ... Can be manipulated doFirst, doLast Can inherits from another type Can depend on another task dependsOn, finalizedBy BASICS The Gradle Task A build = A task graph
  4. Is a Gradle project Basically, a Groovy project It contains

    A build.gradle A plugin class A descriptor One or several tasks An extension Examples Java, Groovy, Maven, Android plugin BASICS The Binary Plugin
  5. BASICS Initialization Choose project(s) to build Configuration Execute build.gradle Build

    task graph Execution Execute task chain Gradle build lifecycle
  6. EXTEND IT Readable The user can easily understand Flexible Express

    complex situations, on a simple way Intuitive The user can easily configure Talkative Help the user solve his problems What makes a good DSL
  7. class MyExtension { String myInfo List<String> myList MyExtension(def arg1, def

    arg2) { myInfo = “Default String” myList = [“default”, “list”] } } READABLE Extension class
  8. READABLE genymotion { //configure genymotion configLicenseServer true configLicenseServerAddress “192.168.1.33” configSdkPath

    “/home/me/Android/sdk” configUseCustomSdk true //launch devices device(“Nexus5”, “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920”) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”) } build.gradle
  9. READABLE genymotion { config { licenseServer true licenseServerAddress “192.168.1.33” sdkPath

    “/home/me/Android/sdk” useCustomSdk true } //launch devices device(“Nexus5”, “Google Nexus 5 - 5.0.0 - API 21 - 1080x1920”) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”) } build.gradle
  10. FLEXIBLE genymotion { //launch devices device(“Nexus5”, “Google Nexus 5 -

    5.0.0 - API 21 - 1080x1920”) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”) } build.gradle
  11. FLEXIBLE genymotion { device(“Nexus5”, “Google Nexus 5 - 5.0.0 -

    API 21 - 1080x1920”, 1920, 1080, “xxhdpi”, ...) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”, 1280, 800, “xhdpi”, ...)) } build.gradle
  12. FLEXIBLE genymotion { device(“Nexus5”, “Google Nexus 5 - 5.0.0 -

    API 21 - 1080x1920”, 1920, 1080, “xxhdpi”, [“path/to/apk”, “path/to/apk2”], [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/database.db”:”/tmp/], true) device(“Nexus4”, “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”, 1280, 800, “xhdpi”, “path/to/apk”, [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”], true) } build.gradle
  13. FLEXIBLE build.gradle genymotion { device(name: “Nexus5”, template: “Google Nexus 5

    - 5.0.0 - API 21 - 1080x1920”, width: 1920, height: 1080, density: “xxhdpi”, install: [“path/to/apk”, “path/to/apk2”], pullAfter: [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”], stopWhenFinished: true) device(name: “Nexus4”, template: “Google Nexus 4 - 4.4.4 - API 19 - 768x1280”, ...) }
  14. FLEXIBLE genymotion { devices { Nexus5 { template “Google Nexus

    5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } } } build.gradle
  15. FLEXIBLE genymotion { devices { Nexus5 { template “Google Nexus

    5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } } } build.gradle project.genymotion.devices
  16. FLEXIBLE genymotion { devices { Nexus5 { template “Google Nexus

    5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } } } build.gradle project.genymotion.devices(Closure c)
  17. FLEXIBLE genymotion { devices { Nexus5 { template “Google Nexus

    5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } } } build.gradle project.genymotion.devices(Closure c) Add ‘Nexus4’ Add ‘Nexus5’
  18. FLEXIBLE genymotion { devices { Nexus5 { template “Google Nexus

    5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } Nexus4 { ... } } } build.gradle project.genymotion.devices(Closure c) Add ‘Nexus4’ Add ‘Nexus5’ Container
  19. FLEXIBLE class GenymotionPlugin implements Plugin<Project> { void apply(Project project) {

    def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch, new DeviceLaunchFactory(instantiator)) project.extensions.create(“genymotion”, GenymotionExtension, project, deviceLaunches) project.afterEvaluate { project.genymotion.injectTasks() } } } The Plugin class
  20. FLEXIBLE class GenymotionPlugin implements Plugin<Project> { void apply(Project project) {

    def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch, new DeviceLaunchFactory(instantiator)) project.extensions.create(“genymotion”, GenymotionExtension, project, deviceLaunches) project.afterEvaluate { project.genymotion.injectTasks() } } } The Plugin class
  21. FLEXIBLE class GenymotionPlugin implements Plugin<Project> { void apply(Project project) {

    def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch, new DeviceLaunchFactory(instantiator)) project.extensions.create(“genymotion”, GenymotionExtension, project, deviceLaunches) project.afterEvaluate { project.genymotion.injectTasks() } } } The Plugin class Create a container for DeviceLaunch
  22. FLEXIBLE class GenymotionPlugin implements Plugin<Project> { void apply(Project project) {

    def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch, new DeviceLaunchFactory(instantiator)) project.extensions. create(“genymotion”, GenymotionExtension, project, deviceLaunches) project.afterEvaluate { project.genymotion.injectTasks() } } } The Plugin class Create the extension
  23. FLEXIBLE class GenymotionPlugin implements Plugin<Project> { void apply(Project project) {

    def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch, new DeviceLaunchFactory(instantiator)) project.extensions.create(“genymotion”, GenymotionExtension, project, deviceLaunches) project.afterEvaluate { project.genymotion.injectTasks() } } } The Plugin class Add the DeviceLaunch container
  24. FLEXIBLE class GenymotionPlugin implements Plugin<Project> { void apply(Project project) {

    def instantiator = project.gradle.services.get(Instantiator) def deviceLaunches = project.container(DeviceLaunch, new DeviceLaunchFactory(instantiator)) project.extensions.create(“genymotion”, GenymotionExtension, project, deviceLaunches) project.afterEvaluate { project.genymotion.injectTasks() } } } The Plugin class
  25. FLEXIBLE The Extension class class GenymotionExtension { NamedDomainObjectContainer<DeviceLaunch> deviceLaunches GenymotionExtension(Project

    project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches } def devices(Closure closure) { deviceLaunches.configure(closure) } ...
  26. FLEXIBLE The Extension class class GenymotionExtension { NamedDomainObjectContainer<DeviceLaunch> deviceLaunches GenymotionExtension(Project

    project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches } def devices(Closure closure) { deviceLaunches.configure(closure) } ... DeviceLaunch container
  27. FLEXIBLE The Extension class class GenymotionExtension { NamedDomainObjectContainer<DeviceLaunch> deviceLaunches GenymotionExtension(Project

    project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches } def devices(Closure closure) { deviceLaunches.configure(closure) } ... We get it from plugin apply()
  28. FLEXIBLE The Extension class class GenymotionExtension { NamedDomainObjectContainer<DeviceLaunch> deviceLaunches GenymotionExtension(Project

    project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches } def devices(Closure closure) { deviceLaunches.configure(closure) } ... Create the syntax genymotion.devices{ }
  29. FLEXIBLE The Extension class class GenymotionExtension { NamedDomainObjectContainer<DeviceLaunch> deviceLaunches GenymotionExtension(Project

    project, deviceLaunches) { this.project = project this.deviceLaunches = deviceLaunches } def devices(Closure closure) { deviceLaunches.configure(closure) } ... Let Gradle add all the declared items
  30. FLEXIBLE The Extension class class DeviceLaunchFactory implements NamedDomainObjectFactory<DeviceLaunch> { final

    Instantiator instantiator public DeviceLaunchFactory(Instantiator instantiator) { this.instantiator = instantiator } @Override DeviceLaunch create(String name) { return instantiator.newInstance(DeviceLaunch.class, name) } }
  31. FLEXIBLE class DeviceLaunchFactory implements NamedDomainObjectFactory<DeviceLaunch> { final Instantiator instantiator public

    DeviceLaunchFactory(Instantiator instantiator) { this.instantiator = instantiator } @Override DeviceLaunch create(String name) { return instantiator.newInstance(DeviceLaunch.class, name) } } The Extension class
  32. INTUITIVE genymotion { devices { Nexus5 { template “Google Nexus

    5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } } } build.gradle
  33. INTUITIVE genymotion { devices { Nexus5 { template “Google Nexus

    5 - 5.0.0 - API 21 - 1080x1920” width 1920 height 1080 density “xxhdpi” install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”] stopWhenFinished true } } } build.gradle
  34. INTUITIVE install “path/to/apk”, “path/to/apk2”, ... pullAfter from:“/sdcard/prop.txt”, to:”/tmp/” pullAfter from:“/sdcard/data.db”,

    to:”/tmp/” build.gradle install [“path/to/apk”, “path/to/apk2”] pullAfter [“/sdcard/prop.txt”:”/tmp/”, “/sdcard/data.db”:”/tmp/”]
  35. INTUITIVE The Extension class (1/2) class GenymotionExtension { private List<String>

    install = [] def install(String... paths) { install.addAll(paths) } def setInstall(String... paths) { install.clear() install.addAll(paths) } ...
  36. INTUITIVE The Extension class (2/2) ... private def pullAfter =

    [:] def pullAfter(String from, String to) { pullAfter.put(from, to) } ... }
  37. TALKATIVE Log is your voice But respect the Gradle conventioned

    levels Errors are part of documentation Anticipate the mistakes and deliver the appropriate explicit message Be Talkative
  38. ANDROID GRADLE PLUGIN android.applicationVariants Only for the app plugin android.libraryVariants

    Only for the library plugin android.testVariants For both plugins The entry points
  39. ANDROID GRADLE PLUGIN Your debugger Browsing through the project on-the-fly

    Integration tests... ... are highly recommended The real documentation part 2
  40. android.testVariants.all { variant -> Task testTask = variant.connectedAndroidTest ... }

    ANDROID GRADLE PLUGIN $ gradle test --stacktrace groovy.lang.MissingPropertyException: Could not find property 'connectedAndroidTest' ... BUILD FAILED
  41. variant.variantData.connectedTestTask = "task ':connectedDebugAndroidTest'" ANDROID GRADLE PLUGIN @Override public DefaultTask

    getConnectedInstrumentTest() { return variantData.connectedTestTask; } Using the debugger TestVariantImpl.java
  42. variant.variantData.connectedTestTask = "task ':connectedDebugAndroidTest'" ANDROID GRADLE PLUGIN @Override public DefaultTask

    getConnectedInstrumentTest() { return variantData.connectedTestTask; } Using the debugger TestVariantImpl.java Task testTask = variant.connectedInstrumentTest The good API
  43. ANDROID GRADLE PLUGIN Do not depend on a specific release

    Integration tests... ... are highly recommended Internals are changing a lot
  44. Very simple As simple as Groovy is Groovy is your

    best friend Very easy to mock Junit & co As anybody knows TEST IT Gradle project testing
  45. ProjectBuilder To create a project stub Evaluate To execute your

    build script TEST IT A few specificities
  46. TEST IT Our buid.gradle ... repositories { mavenCentral() } dependencies

    { testCompile 'junit:junit:4.11' } ... Adding maven central repository
  47. TEST IT Our buid.gradle ... repositories { mavenCentral() } dependencies

    { testCompile 'junit:junit:4.11' } ... Adding junit as testing dependency
  48. TEST IT Your first test! class GenymotionPluginTest { @Test public

    void canAddGenymotionExtension() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion' assert project.genymotion instanceof GenymotionExtension } }
  49. TEST IT Your first test! class GenymotionPluginTest { @Test public

    void canAddGenymotionExtension() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion' assert project.genymotion instanceof GenymotionExtension } } Stub a Gradle project
  50. TEST IT Your first test! class GenymotionPluginTest { @Test public

    void canAddGenymotionExtension() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion' assert project.genymotion instanceof GenymotionExtension } } Apply our plugin
  51. TEST IT Your first test! class GenymotionPluginTest { @Test public

    void canAddGenymotionExtension() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion' assert project.genymotion instanceof GenymotionExtension } } Test our extension exists
  52. TEST IT Your second test! class GenymotionPluginTest { @Test public

    void canAddGenymotionTask() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion' assert project.tasks.genymotionTask instanceof GenymotionTask } } We initialize our project
  53. TEST IT Your second test! class GenymotionPluginTest { @Test public

    void canAddGenymotionTask() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion' assert project.tasks.genymotionTask instanceof GenymotionTask } } We test the task
  54. TEST IT Run your second test $ gradle test --stacktrace

    --debug ... com.genymotion.GenymotionPluginTest > canAddGenymotionTask FAILED MissingPropertyException:Could not find property 'genymotionTask' on task set ... BUILD FAILED
  55. TEST IT Run your second test $ gradle test --stacktrace

    --debug ... com.genymotion.GenymotionPluginTest > canAddGenymotionTask FAILED MissingPropertyException: Could not find property 'genymotionTask' on task set ... BUILD FAILED Our task is not created
  56. class GenymotionPlugin implements Plugin<Project> { void apply(Project project) { //create

    extensions ... project.afterEvaluate { //create the tasks ... } } } TEST IT The Plugin class Tasks are created after project. evaluate()
  57. TEST IT Your first test! class GenymotionPluginTest { @Test public

    void canAddGenymotionTask() { Project project = ProjectBuilder.builder().build() project.apply plugin: 'genymotion' project.evaluate() assert project.tasks.genymotionTask instanceof GenymotionTask } } We launch evaluate() on the project
  58. TEST IT LUKE DALEY Gradleware Principal Engineer GRADLE FORUM You

    don't see this in the API docs for Project because it is an internal method and is therefore potentially subject to change in future releases. There will be a supported mechanism for doing this kind of thing in the near future. ” “
  59. TEST IT LUKE DALEY Gradleware Principal Engineer GRADLE FORUM You

    don't see this in the API docs for Project because it is an internal method and is therefore potentially subject to change in future releases. There will be a supported mechanism for doing this kind of thing in the near future. June 2011 ” “
  60. TEST IT build.gradle repositories { jcenter() } dependencies { testCompile

    'junit:junit:4.11' testCompile "com.android.tools.build:gradle:1.3.1" }
  61. TEST IT Project project = ProjectBuilder.builder() .withProjectDir(new File("res/test/android-app")) .build(); project.apply

    plugin: 'com.android.application' project.apply plugin: 'genymotion' project.android { compileSdkVersion 21 buildToolsVersion "21.1.2" } Test class We create a project from a folder
  62. TEST IT Project project = ProjectBuilder.builder() .withProjectDir(new File("res/test/android-app")) .build(); project.apply

    plugin: 'com.android.application' project.apply plugin: 'genymotion' project.android { compileSdkVersion 21 buildToolsVersion "21.1.2" } Test class We add the Android Gradle plugin
  63. TEST IT Project project = ProjectBuilder.builder() .withProjectDir(new File("res/test/android-app")) .build(); project.apply

    plugin: 'com.android.application' project.apply plugin: 'genymotion' project.android { compileSdkVersion 21 buildToolsVersion "21.1.2" } Test class We declare the mandatory values
  64. TEST IT @Test @Category(Android) public void canInjectToVariants() { project =

    getAndroidProject() project.android.productFlavors { flavor1 flavor2 } project.evaluate() ... Test class (1/2) We annotate Android related tests
  65. TEST IT @Test @Category(Android) public void canInjectToVariants() { project =

    getAndroidProject() project.android.productFlavors { flavor1 flavor2 } project.evaluate() ... Test class (1/2) We add flavors to the project
  66. TEST IT ... project.android.testVariants.all { variant -> Task connectedTask =

    variant.connectedInstrumentTest assert connectedTask.getTaskDependencies().getDependencies() .contains(genymotionTask) } } Test class (2/2) We test the dependency is done
  67. Test with several Android plugin versions Control Android plugin version

    from outside the project Use Gradle properties TEST IT Ensure compatibility
  68. TEST IT def androidVersion = "+" if (hasProperty("androidPluginVersion")) { androidVersion

    = androidPluginVersion } dependencies { testCompile 'junit:junit:4.11' testCompile "com.android.tools.build:gradle: $androidVersion" } build.gradle ./gradlew test -PandroidPluginVersion=1.3.1 cmd
  69. Run Android integration tests daily On your CI Test with

    the beta releases Use jcenter() Set the default plugin version to “+” TEST IT Ensure compatibility
  70. PUBLISH IT Sharing with people Being public Easy embbeding On

    the build.gradle Why publishing your plugin?
  71. PUBLISH IT Host code on github Open Source Host binary

    on bintray bintray.com Referenced on JCenter jcenter() How to? The quick way, for free