Build / Gradle / Kotlin / Muselee

Muselee 4: Gradle Kotlin DSL

Muselee is a demo app which allows the user to browse popular music artists. It is not intended to be a fully-featured user app, but a vehicle to explore good app architecture, how to implement current best-practice, and explore how the two often go hand in hand. Moreover it will be used to explore how implementing some specific patterns can help to keep our app both maintainable, and easy to extend.

So far we’ve looked at our basic three project structure, plus how we can use dependency injection to support this, and also a method of consistently managing our third-party library dependencies to ensure that we are using consistent versions in each of our modules. With regard to this final area, what we did previously works quite well, but that doesn’t mean we can’t improve it! One of the problems with it is that Android Studio / IntelliJ IDEA don’t handle auto-completion that well for Groovy scripts, which is what we’re using in our build config. However there is a really rather nice alternative, and in this post we’ll look at converting our build scripts to use the Gradle Kotlin DSL instead.

The Gradle Kotlin DSL allows us to create our build scripts in Kotlin rather than Groovy. As a result we get much better IDE support because Kotlin is a first class language in both Android Studio and IntelliJ IDEA, of which AS is a fork (Jetbrains develop both IntelliJ IDEA and the initial developers of Kotlin, so this is hardly surprising). The benefits stretch further than just better IDE integration – having your build scripts in the same language as your codebase is actually pretty nice! That said, there are some issues: Firstly, it can be a little tricky to convert your existing config but hopefully this post should help; Secondly, you still need an understanding of Gradle and it’s various structures to use the Gradle Kotlin DSL effectively; and thirdly Android Studio project templates don’t yet support the Gradle Kotlin DSL, so will only create Groovy files which you’ll need to convert manually.

While some would argue that I should have done this conversion in the first article where I showed the dependency version management, I deliberately chose to break this in to separate posts. The reasoning behind this was twofold. Firstly I wanted each post to focus on a single area to make things easier to understand for the reader; and secondly, I felt that there was more value in showing how to convert existing Groovy scripts to Kotlin ones as this is something that many developers may need to do.

The first thing that we need to consider is how we’ll define the common dependencies that we previously created in dependencies.gradle. For this we essentially added all of our dependencies to the ext structure, but there’s actually a better way that we can do this, by using a Kotlin buildSrcfolder. This folder is nothing specific to the Kotlin DSL, but is a Gradle feature that we can use with Groovy as well. It enables us to define tasks and tools that are available throughout the build scripts, and we can use that to make our version information available throughout our build scripts. But better still we can use Kotlin within this folder with a small amount of set up. To enable Kotlin we just need to specify the kotlin-dsl plugin within the build config for the buildSrc folder:

repositories {
    jcenter()
}

plugins {
    `kotlin-dsl`
}

kotlinDslPluginOptions {
    experimentalWarning.set(false)
}

This applies the kotlin-dsl, declares the repository from which is can be obtained, and disables a warning that it is still experimental.

One thing worth noting is that this file is has a .kts suffix, and that indicates to Gradle this this file is a Kotlin script and not a Groovy one. As we progress we’ll convert all of our scripts to Kotlin.

The next thing that we need to do is create our dependency definitions:

const val kotlinVersion = "1.3.11"

object BuildPlugins {
    object Versions {
        const val androidBuildToolsVersion = "3.4.0-alpha09"
        const val detekt = "1.0.0-RC12"
    }

    const val androidGradlePlugin = "com.android.tools.build:gradle:${Versions.androidBuildToolsVersion}"
    const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"

    const val androidApplication = "com.android.application"
    const val androidLibrary = "com.android.library"
    const val kotlinAndroid = "kotlin-android"
    const val kotlinAndroidExtensions = "kotlin-android-extensions"
    const val kotlinKapt = "kotlin-kapt"
    const val detekt = "io.gitlab.arturbosch.detekt"
}

object AndroidSdk {
    const val min = 21
    const val compile = 28
    const val target = compile
}

object ProjectModules {
    const val core = ":core"
    const val topartists = ":topartists"
}

object Libraries {
    private object Versions {
        const val jetpack = "1.1.0-alpha01"
        const val constraintLayout = "2.0.0-alpha3"
        const val ktx = "1.1.0-alpha03"
        const val dagger = "2.20"
    }

    const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
    const val appCompat = "androidx.appcompat:appcompat:${Versions.jetpack}"
    const val constraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintLayout}"
    const val ktxCore = "androidx.core:core-ktx:${Versions.ktx}"
    const val dagger = "com.google.dagger:dagger:${Versions.dagger}"
    const val daggerAndroid = "com.google.dagger:dagger-android-support:${Versions.dagger}"
    const val daggerCompiler = "com.google.dagger:dagger-compiler:${Versions.dagger}"
    const val daggerAndroidCompiler = "com.google.dagger:dagger-android-processor:${Versions.dagger}"
}

object TestLibraries {
    private object Versions {
        const val junit4 = "4.13-beta-1"
    }
    const val junit4 = "junit:junit:${Versions.junit4}"
}

Although this file is quite large, this is pure Kotlin and it pretty easy to understand – it is a declaration of some objects which contain constants for our dependencies. This file is not a Kotlin script, it is simply Kotlin source which gets compiled and is available throughout our build scripts. When we next execute a build, Gadle will detect the presence of the buildSrc folder, and will compile the contents, and make those compiled objects available to our build scripts. We can actually stop there, if we want. we can delete the existing dependencies.gradle file that we created previously, and change our existing Groovy scripts to use these values instead:

plugins {
    id BuildPlugins.detekt version BuildPlugins.Version.detekt
}

apply plugin: BuildPlugins.androidLibrary
apply plugin: BuildPlugins.kotlinAndroidExtensions
apply plugin: BuildPlugins.kotlinAndroid

android {
    compileSdkVersion AndroidSdk.compile

    defaultConfig {
        minSdkVersion AndroidSdk.min
        targetSdkVersion AndroidSdk.target

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation Libraries.kotlinStdLib

    testImplementation TestLibraries.junit4
}

detekt {
    version = BuildPlugins.Version.detekt
    input = files("src/main/java", "src/androidx/java", "src/support/java")
    filters = ".*test.*,.*/resources/.*,.*/tmp/.*"
}

These are now directly referencing the objects that we defined earlier in Dependencies.kt. It is worth noting that because the values that we are referencing are Kotlin values, we will now get auto-completion even when we consume them within a Groovy script.

But we’re not done yet. We can now convert this script to use the Gradle Kotlin DSL instead. While the differences are only subtle, if we wanted to declare a custom task, we could write the task body in Kotlin, which would make life much easier.

There are a couple of potential pitfalls when it comes to performing this conversion, so we’ll go through things in stages.

To begin performing the conversion, we need to rename the file to build.gradle.kts – adding the .kts extension, then let’s consider the plugins. Using the Korlin DL we can actually consolidate these:

plugins {
    id(BuildPlugins.androidLibrary)
    id(BuildPlugins.kotlinAndroid)
    id(BuildPlugins.kotlinAndroidExtensions)
    id(BuildPlugins.detekt) version (BuildPlugins.Versions.detekt)
}

The main thing worth noting, aside from the fact that now everything is within a single plugins block is that id and version are actually a function calls, so the arguments need to be in parentheses. This is a common motif that we’ll see through the conversion process.

Next let’s look at the Android SDK versions:

android {
    compileSdkVersion(AndroidSdk.compile)

    defaultConfig {
        minSdkVersion(AndroidSdk.min)
        targetSdkVersion(AndroidSdk.target)
        ...
    }
    ...
}

Once again, the difference here is that we need to add parentheses to make function calls. When specifying the testInstrumentationRunner we need to add an = to set a var (even though we don’t have any instrumentation tests, I’ve left this in to show how we would need to make this conversion):

        testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"

Specifying configuration settings for buildTypes requires a little bit more work. We have to look up the specific build type:

 
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }

We also need to perform an assignment for isMinifyEnabled and turn both proguardFiles and getDefaultProguardFile in to function calls.

Finally we turn our dependencies in to function calls:

dependencies {
    implementation(Libraries.kotlinStdLib)

    testImplementation(TestLibraries.junit4)
}

The detekt block does not require any updates.

I’ve only shown this conversion for the core module, but same techniques are required for the others. We now have Kotlin scripts throughout, and this will please all Kotlin-loving Android developers!

It’s worth mentioning that there are some great blog posts which go in to more detail than I have here. There’s the official migration guide which is not Android-specific, but still provides some good insights. Antonio Leiva has written a really good script conversion walk-through. Sam Edwards has written a guide for handling dependency management using various techniques, including a Kotlin buildSrc file. Finally, a big hat tip to Alex Saveau for a Twitter conversation which prompted me to consider making this change for Muselee.

In the next article we’ll start looking at our topartists feature module, and look at the architecture that we’ll use for this.

The source code for this article is available here.

© 2019, Mark Allison. All rights reserved.

Copyright © 2019 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.