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

Kotlinify your Gradle

Kotlinify your Gradle

Presentation given at Android Makers Paris 2018 https://androidmakers.fr/schedule/#session-45

Alexander Gherschon

April 23, 2018
Tweet

More Decks by Alexander Gherschon

Other Decks in Programming

Transcript

  1. WHO AM I? • Android Developer @ Houzz • Organiser

    and Speaker @ KotlinTLV.co.il • Love to build tools!
  2. WHAT IS GRADLE • Build system (JVM based) • Defines

    Tasks inside a Project • A Plugin is set of Tasks gathered for a purpose • Java, Kotlin, Android, iOS, .Net…
  3. INTERESTING EXISTING PLUGINS • Android Gradle Plugin • Kotlin Gradle

    Plugin • Gradle Android Ribbonizer plugin • Gradle Play Publisher • etc…
  4. BUILDING A GRADLE PLUGIN • Print all “// TODO” lines

    in our project • Find all source files • Find all lines starting with “// TODO” • Android projects (for each build variant)
  5. USING GRADLE PLUGINS buildscript { ext.kotlin_version = '1.2.21' repositories {

    google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath “org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version” } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android'
  6. buildscript { ext.kv = '1.2.21' repositories { google() jcenter() }

    dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kv" } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' USING GRADLE PLUGINS
  7. buildscript { ext.kv = '1.2.21' repositories { google() jcenter() }

    dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kv" } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' USING GRADLE PLUGINS
  8. buildscript { ext.kv = '1.2.21' repositories { google() jcenter() }

    dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kv" } } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' USING GRADLE PLUGINS
  9. ADDING DEPENDENCIES ./build.gradle repositories { jcenter() google() } dependencies {

    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile group: 'junit', name: 'junit', version: '4.12' compile gradleApi() compileOnly 'com.android.tools.build:gradle:3.0.1' }
  10. ADDING DEPENDENCIES ./build.gradle repositories { jcenter() google() } dependencies {

    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile group: 'junit', name: 'junit', version: '4.12' compile gradleApi() compileOnly 'com.android.tools.build:gradle:3.0.1' }
  11. ADDING DEPENDENCIES ./build.gradle repositories { jcenter() google() } dependencies {

    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile group: 'junit', name: 'junit', version: '4.12' compile gradleApi() compileOnly 'com.android.tools.build:gradle:3.0.1' }
  12. ADDING DEPENDENCIES ./build.gradle repositories { jcenter() google() } dependencies {

    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile group: 'junit', name: 'junit', version: '4.12' compile gradleApi() compileOnly 'com.android.tools.build:gradle:3.0.1' }
  13. DEPLOYING LOCALLY ./build.gradle apply plugin: 'maven-publish' publishing { publications {

    TodosPublication(MavenPublication) { groupId = pluginGroupId artifactId = pluginArtifactId version = pluginVersion from components.java } } }
  14. DEPENDING & APPLYING ./build.gradle buildscript { repositories { mavenLocal() }

    dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "il.co.galex.tools.build:todos:0.0.1" } } apply plugin: 'il.co.galex.todos' ./app/build.gradle
  15. DEPENDING & APPLYING ./build.gradle buildscript { repositories { mavenLocal() }

    dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "il.co.galex.tools.build:todos:0.0.1" } } apply plugin: 'il.co.galex.todos' ./app/build.gradle
  16. IMPLEMENTING OUR PLUGIN class TodosPlugin: Plugin<Project> { override fun apply(project:

    Project?) { TODO("We will implement here our plugin!") } }
  17. IMPLEMENTING OUR PLUGIN override fun apply(project: Project) { val hasApp

    = project.plugins.hasPlugin(AppPlugin::class.java) val hasLib = project.plugins.hasPlugin(LibraryPlugin::class.java) if (!hasApp && !hasLib) { throw IllegalStateException(“'some warning here”) } (…) }
  18. DEPENDENCIES (REMINDER) ./build.gradle repositories { jcenter() google() } dependencies {

    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile group: 'junit', name: 'junit', version: '4.12' compile gradleApi() compileOnly 'com.android.tools.build:gradle:3.0.1' }
  19. DEPENDENCIES (REMINDER) ./build.gradle repositories { jcenter() google() } dependencies {

    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompile group: 'junit', name: 'junit', version: '4.12' compile gradleApi() compileOnly 'com.android.tools.build:gradle:3.0.1' }
  20. IMPLEMENTING OUR PLUGIN override fun apply(project: Project) { val hasApp

    = project.plugins.hasPlugin(AppPlugin::class.java) val hasLib = project.plugins.hasPlugin(LibraryPlugin::class.java) if (!hasApp && !hasLib) { throw IllegalStateException(“'some warning here”) } (…) }
  21. IMPLEMENTING OUR PLUGIN override fun apply(project: Project) { val hasApp

    = project.plugins.hasPlugin(AppPlugin::class.java) val hasLib = project.plugins.hasPlugin(LibraryPlugin::class.java) if (!hasApp && !hasLib) { throw IllegalStateException(“'some warning here”) } (…) }
  22. IMPLEMENTING OUR PLUGIN override fun apply(project: Project) { val hasApp

    = project.plugins.hasPlugin(AppPlugin::class.java) val hasLib = project.plugins.hasPlugin(LibraryPlugin::class.java) if (!hasApp && !hasLib) { throw IllegalStateException(“'some warning here”) } (…) }
  23. IMPLEMENTING OUR PLUGIN override fun apply(project: Project) { (…) val

    variants: DomainObjectSet<out BaseVariant> = if (hasApp) { project.extensions.findByType(AppExtension::class.java)!!.applicationVariants } else { project.extensions.findByType(LibraryExtension::class.java)!!.libraryVariants } (…) }
  24. IMPLEMENTING OUR PLUGIN override fun apply(project: Project) { (…) val

    variants: DomainObjectSet<out BaseVariant> = if (hasApp) { project.extensions.findByType(AppExtension::class.java)!!.applicationVariants } else { project.extensions.findByType(LibraryExtension::class.java)!!.libraryVariants } (…) }
  25. IMPLEMENTING OUR PLUGIN override fun apply(project: Project) { val variants:

    DomainObjectSet<out BaseVariant> = (…) variants.all { variant: BaseVariant -> variant.sourceSets.flatMap { it.javaDirectories } .forEach { println(it) } } }
  26. IMPLEMENTING OUR PLUGIN override fun apply(project: Project) { val variants:

    DomainObjectSet<out BaseVariant> = (…) variants.all { variant: BaseVariant -> variant.sourceSets.flatMap { it.javaDirectories } .forEach { println(it) } } }
  27. IMPLEMENTING OUR PLUGIN variants.all { variant: BaseVariant -> val folders

    = variant.sourceSets.flatMap { it.javaDirectories } val taskName = "print${variant.name.capitalize()}Todos" val task = project.tasks.create(taskName, TodosTask::class.java) { it.folders = folders } task.group = "Todos" task.description = "Find all todos in the project for variant ${variant.name}" }
  28. IMPLEMENTING OUR PLUGIN variants.all { variant: BaseVariant -> val folders

    = variant.sourceSets.flatMap { it.javaDirectories } val taskName = "print${variant.name.capitalize()}Todos" val task = project.tasks.create(taskName, TodosTask::class.java) { it.folders = folders } task.group = "Todos" task.description = "Find all todos in the project for variant ${variant.name}" }
  29. IMPLEMENTING OUR PLUGIN variants.all { variant: BaseVariant -> val folders

    = variant.sourceSets.flatMap { it.javaDirectories } val taskName = "print${variant.name.capitalize()}Todos" val task = project.tasks.create(taskName, TodosTask::class.java) { it.folders = folders } task.group = "Todos" task.description = "Find all todos in the project for variant ${variant.name}" }
  30. IMPLEMENTING OUR PLUGIN variants.all { variant: BaseVariant -> val folders

    = variant.sourceSets.flatMap { it.javaDirectories } val taskName = "print${variant.name.capitalize()}Todos" val task = project.tasks.create(taskName, TodosTask::class.java) { it.folders = folders } task.group = "Todos" task.description = "Find all todos in the project for variant ${variant.name}" }
  31. IMPLEMENTING OUR PLUGIN variants.all { variant: BaseVariant -> val folders

    = variant.sourceSets.flatMap { it.javaDirectories } val taskName = "print${variant.name.capitalize()}Todos" val task = project.tasks.create(taskName, TodosTask::class.java) { it.folders = folders } task.group = "Todos" task.description = "Find all todos in the project for variant ${variant.name}" }
  32. IMPLEMENTING OUR PLUGIN variants.all { variant: BaseVariant -> val folders

    = variant.sourceSets.flatMap { it.javaDirectories } val taskName = "print${variant.name.capitalize()}Todos" val task = project.tasks.create(taskName, TodosTask::class.java) { it.folders = folders } task.group = "Todos" task.description = "Find all todos in the project for variant ${variant.name}" }
  33. IMPLEMENTING OUR PLUGIN variants.all { variant: BaseVariant -> val folders

    = variant.sourceSets.flatMap { it.javaDirectories } val taskName = "print${variant.name.capitalize()}Todos" val task = project.tasks.create(taskName, TodosTask::class.java) { it.folders = folders } task.group = "Todos" task.description = "Find all todos in the project for variant ${variant.name}" }
  34. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  35. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  36. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  37. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  38. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  39. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  40. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  41. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  42. THE TODOS TASK open class TodosTask : DefaultTask() { @TaskAction

    fun run() { folders?.forEach { folder -> folder.walk().filter { it.isFile }.forEach { file -> file.readLines().forEachIndexed { index, line -> if (line.contains("// TODO")) { println("$file:${index + 1} - ${line.trim()}") } } } } } }
  43. WRITING OUR EXTENSION open class TodosExtension { var keywords: List<String>?

    = null set(value) { when { value == null -> throw IllegalArgumentException("...") value.isEmpty() -> throw IllegalArgumentException("...") value.find { it == "" } != null -> throw IllegalArgumentException("...") else -> field = value } } }
  44. WRITING OUR EXTENSION open class TodosExtension { var keywords: List<String>?

    = null set(value) { when { value == null -> throw IllegalArgumentException("...") value.isEmpty() -> throw IllegalArgumentException("...") value.find { it == "" } != null -> throw IllegalArgumentException("...") else -> field = value } } }
  45. WRITING OUR EXTENSION open class TodosExtension { var keywords: List<String>?

    = null set(value) { when { value == null -> throw IllegalArgumentException("...") value.isEmpty() -> throw IllegalArgumentException("...") value.find { it == "" } != null -> throw IllegalArgumentException("...") else -> field = value } } }
  46. USING OUR EXTENSION IN OUR PLUGIN class TodosPlugin : Plugin<Project>

    { override fun apply(project: Project) { project.extensions.create("todos", TodosExtension::class.java) } }
  47. USING OUR EXTENSION IN OUR PLUGIN class TodosPlugin : Plugin<Project>

    { override fun apply(project: Project) { project.extensions.create("todos", TodosExtension::class.java) } }
  48. USING OUR EXTENSION IN OUR TASK val extension: TodosExtension? =

    project.extensions.findByType(TodosExtension::class.java) val keywords = extension?.keywords ?: listOf(“TODO") file.readLines().forEachIndexed { index, line -> if (line.trim().startsWith("//") && keywords.intersect(line.split(" ")).isNotEmpty()) { println("$file:${index + 1} - ${line.trim()}") } }
  49. USING OUR EXTENSION IN OUR TASK val extension: TodosExtension? =

    project.extensions.findByType(TodosExtension::class.java) val keywords = extension?.keywords ?: listOf(“TODO") file.readLines().forEachIndexed { index, line -> if (line.trim().startsWith("//") && keywords.intersect(line.split(" ")).isNotEmpty()) { println("$file:${index + 1} - ${line.trim()}") } }
  50. USING OUR EXTENSION IN OUR TASK val extension: TodosExtension? =

    project.extensions.findByType(TodosExtension::class.java) val keywords = extension?.keywords ?: listOf(“TODO") file.readLines().forEachIndexed { index, line -> if (line.trim().startsWith("//") && keywords.intersect(line.split(" ")).isNotEmpty()) { println("$file:${index + 1} - ${line.trim()}") } }
  51. • Built a Gradle Plugin • Created a task per

    Android Build Variant • Used an extension for as plugin parameters RECAP
  52. SIMPLE DSL EXAMPLE enum class Color { BLUE, RED, GREEN

    } data class Wheels(var number: Int? = null, var color: Color? = null) data class Vehicle(var wheels: Wheels? = null, var engine: String? = null)
  53. SIMPLE DSL EXAMPLE // imperative way val carImperative = Vehicle(Wheels(2,

    Color.RED), "Meh") println(carImperative) // Vehicle(wheels=Wheels(number=2, color=RED), engine=Meh)
  54. SIMPLE DSL EXAMPLE // declarative way val carDSL = vehicle

    { engine = "V8" wheels { number = 4 color = Color.BLUE } }
  55. SIMPLE DSL EXAMPLE private fun vehicle(block: Vehicle.() -> Unit) =

    Vehicle().apply(block) // declarative way val carDSL = vehicle { engine = "V8" wheels { number = 4 color = Color.BLUE } } SIMPLE DSL EXAMPLE
  56. SIMPLE DSL EXAMPLE private fun vehicle(block: Vehicle.() -> Unit) =

    Vehicle().apply(block) // declarative way val carDSL = vehicle { engine = "V8" wheels { number = 4 color = Color.BLUE } } SIMPLE DSL EXAMPLE
  57. SIMPLE DSL EXAMPLE // declarative way val carDSL = vehicle

    { this.engine = "V8" wheels { number = 4 color = Color.BLUE } } private fun vehicle(block: Vehicle.() -> Unit) = Vehicle().apply(block) SIMPLE DSL EXAMPLE
  58. THE APPLY FUNCTION private fun vehicle(block: Vehicle.() -> Unit) =

    Vehicle().apply(block) private fun vehicle(block: Vehicle.() -> Unit): Vehicle { val vehicle = Vehicle() vehicle.block() return vehicle }
  59. APPLY FUNCTION private fun vehicle(block: Vehicle.() -> Unit) = Vehicle().apply(block)

    private fun vehicle(block: Vehicle.() -> Unit): Vehicle { val vehicle = Vehicle() vehicle.block() return vehicle }
  60. SIMPLE DSL EXAMPLE // declarative way val carDSL = vehicle

    { engine = "V8" wheels { number = 4 color = Color.BLUE } }
  61. SIMPLE DSL EXAMPLE // declarative way val carDSL = vehicle

    { this.engine = "V8" this.wheels { this.number = 4 this.color = Color.BLUE } }
  62. SIMPLE DSL EXAMPLE private fun Vehicle.wheels(block: Wheels.() -> Unit) {

    this.wheels = Wheels().apply(block) } // declarative way val carDSL = vehicle { this.engine = "V8" this.wheels { this.number = 4 this.color = Color.BLUE } }
  63. SIMPLE DSL EXAMPLE private fun Vehicle.wheels(block: Wheels.() -> Unit) {

    this.wheels = Wheels().apply(block) } // declarative way val carDSL = vehicle { this.engine = "V8" this.wheels { this.number = 4 this.color = Color.BLUE } }
  64. SIMPLE DSL EXAMPLE private fun Vehicle.wheels(block: Wheels.() -> Unit) {

    this.wheels = Wheels().apply(block) } // declarative way val carDSL = vehicle { this.engine = "V8" this.wheels { this.number = 4 this.color = Color.BLUE } }
  65. SIMPLE DSL EXAMPLE private fun Vehicle.wheels((block: Wheels.() -> Unit) {

    this.wheels = Wheels().apply(block) } // declarative way val carDSL = vehicle { this.engine = "V8" this.wheels { this.number = 4 this.color = Color.BLUE } } Lambda with Receiver = Lambda, with inside “this”
  66. WHAT IS THE KOTLIN-DSL • Gradle API • Set of

    Extensions functions and lambdas with receiver • IntelliJ ‘extension’ which understands .kts build files
  67. PS: CONVERT TO K IN TWO GIT ‘PHASES’ 1.Rename file

    then commit 2.Modify its content then commit and push
  68. PS: CONVERT TO K IN TWO GIT ‘PHASES’ 1.build.gradle ->

    build.gradle.kts + commit 2.Modify build.gradle.kts + commit + push
  69. CONVERTING OUR PLUGIN - 1 buildscript { ext.kotlin_version = '1.2.31'

    repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'maven-publish'
  70. CONVERTING OUR PLUGIN - 3 task sourceJar(type: Jar) { from

    sourceSets.main.allJava } task javadocJar(type: Jar, dependsOn: javadoc) { from javadoc.destinationDir }
  71. CONVERTING OUR PLUGIN - 3’ val sourceJar = task<Jar>("sourceJar") {

    from(java.sourceSets["main"].allSource) classifier = "sources" } val javadocJar: Jar = task<Jar>("javadocJar") { dependsOn("javadoc") from(java.docsDir) classifier = "javadoc" }
  72. CONVERTING OUR PLUGIN - 4 ext { pluginGroupId = 'il.co.galex.tools.build'

    pluginArtifactId = 'todos' pluginVersion = '0.0.2' }
  73. CONVERTING OUR PLUGIN - 5 publishing { publications { TodosPublication(MavenPublication)

    { groupId = pluginGroupId artifactId = pluginArtifactId version = pluginVersion from components.java artifact sourceJar { classifier "sources" } artifact javadocJar { classifier = 'javadoc' } } } }
  74. CONVERTING OUR PLUGIN - 5’ publishing { publications { create<MavenPublication>("TodosPublication")

    { groupId = ext["pluginGroupId"] as String? artifactId = ext["pluginArtifactId"] as String? version = ext["pluginVersion"] as String? from(components["java"]) artifact(sourceJar) artifact(javadocJar) } } }
  75. CONVERTING OUR ANDROID APP - 1 android { compileSdkVersion 26

    defaultConfig { applicationId "il.co.galex.todosgradleplugintest" minSdkVersion 15 targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } (...) }
  76. CONVERTING OUR ANDROID APP - 1’ android { compileSdkVersion(26) defaultConfig

    { applicationId = "il.co.galex.todosgradleplugintest" minSdkVersion(15) targetSdkVersion(26) versionCode = 1 versionName = "1.0" testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" } (...) }
  77. CONVERTING OUR ANDROID APP - 2 signingConfigs { debug {

    storeFile file("debug.keystore") } } buildTypes { debug { signingConfig signingConfigs.debug minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }
  78. CONVERTING OUR ANDROID APP - 2’ signingConfigs { getByName("debug") {

    storeFile = file("debug.keystore") } } buildTypes { getByName("debug") { signingConfig = signingConfigs.getByName("debug") isMinifyEnabled = false proguardFile(getDefaultProguardFile ("proguard-android.txt")) proguardFile("proguard-rules.pro") } }
  79. CONVERTING OUR ANDROID APP - 3 sourceSets { main {

    java.srcDirs += 'src/main/kotlin' } debug { java.srcDirs += 'src/debug/kotlin' } release { java.srcDirs += 'src/release/kotlin' } }
  80. CONVERTING OUR ANDROID APP - 3’ sourceSets { getByName("main") {

    this.java.srcDir("src/main/kotlin") } getByName("debug") { this.java.srcDir("src/debug/kotlin") } getByName("release") { this.java.srcDir("src/release/kotlin") } }
  81. CONVERTING OUR ANDROID APP - 4 dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"

    implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' }
  82. CONVERTING OUR ANDROID APP - 4’ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion") implementation("com.android.support:appcompat-v7:26.1.0")

    implementation("com.android.support.constraint:constraint-layout:1.0.2") testImplementation("junit:junit:4.12") androidTestImplementation("com.android.support.test:runner:1.0.1") androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.1") }
  83. RECAP • Converted from Groovy to Kotlin • Not that

    easy to switch • Probably because it’s quite early… v0.1.6.3 • But it’s Kotlin, it’s fun!
  84. • Todos Gradle Plugin https://github.com/galex/android-todos-gradle-plugin • Android Test App https://github.com/galex/android-todos-test-app

    • Some good blogposts on the Kotlin-DSL • https://antonioleiva.com/kotlin-dsl-gradle/ • https://kotlinexpertise.com/gradlekotlindsl/ • Books • Kotlin in Action https://www.manning.com/books/kotlin-in-action • Gradle in Action https://www.manning.com/books/gradle-in-action LINKS