Kotlin DSL simplifies Gradle scripting for Android development.

Kotlin DSL simplifies Gradle scripting for Android development.

If you are scared of Gradle scripts with unfamiliar groovy syntax files, then Kotlin DSL is made for you. It's time to face the Gradle scripts with more confidence. This article will convert some standard Groovy-based Gradle scripts into Kotlin DSL scripts.

Let’s get started

Consider enhancing your half-baked side Android project ??. To do so, create a folder named buildSrc in the root directory of your project, just like your app/ folder. This particular folder enables Gradle scripts at the project level. After creating the buildSrc folder, hit the sync button, and observe the temporary files that are added to it as a result of these changes.

No alt text provided for this image
buildSrc folder

Congratulations, you've completed half the task ??! However, in order to enable Kotlin DSL for your project, there's one more thing you need to do. Let's put our thinking caps on and create a new file inside the buildSrc directory called "build.gradle.kts". Once the file is created, open it and add the necessary code that will instruct Gradle to enable the Kotlin DSL plugin for our project.

import org.gradle.kotlin.dsl.`kotlin-dsl`

plugins {
    `kotlin-dsl`
}

repositories {
    jcenter()
}        

The battle has been won, and Kotlin DSL is now enabled in our project. All that's left is to sync and we're good to go!

No alt text provided for this image

Is there anything remaining to be done? ??

Indeed, the work we've accomplished holds no value unless we apply Kotlin DSL to practical use.

Kotlin DSL simplifies the syntax of the Kotlin language and incorporates its extensive API set directly into script files. With the added benefit of code completion, it's a perfect fit for working with Gradle script files. Leveraging this tool allows for more elegant management of dependencies and project settings configurations. Kotlin DSL files have the .kts extension, which means that all of our .gradle files will be converted to .gradle.kts.

In order to proceed with converting our files to Kotlin DSL, we need to ensure that certain necessary files are in place. To do this, follow these steps:

  1. Create folders inside the buildSrc folder as shown below.
  2. Create the required files within the respective folders.

By completing these steps, we will have the necessary files in place to proceed with the conversion process using Kotlin DSL.

buildSrc->src->main->java        

AppConfig.kt:

//app level config constants
object AppConfig {
    const val compileSdk = 30
    const val minSdk = 21
    const val targetSdk = 30
    const val versionCode = 1
    const val versionName = "1.0.0"
    const val buildToolsVersion = "29.0.3"

    const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner"
    const val proguardConsumerRules =  "consumer-rules.pro"
    const val dimension = "environment"
}        

By having all of our app-level configurations related to the project in one place, this file allows us to effectively manage them.

Versions.kt

//version constants for the Kotlin DSL dependencies
object Versions {
    //app level
    const val gradle = "4.0.1"
    const val kotlin = "1.4.0"

    //libs
    val coreKtx = "1.2.0"
    val appcompat = "1.3.0-alpha01"
    val constraintLayout = "2.0.0-beta8"

    //test
    val junit = "4.12"
    val extJunit = "1.1.1"
    val espresso = "3.2.0"
}        

By utilizing this file, we can centralize the versioning of our libraries and keep them separate from the rest of the code.

AppDependencies.kt

import org.gradle.api.artifacts.dsl.DependencyHandler

object AppDependencies {
    //std lib
    val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.kotlin}"

    //android ui
    private val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
    private val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}"
    private val constraintLayout =
        "androidx.constraintlayout:constraintlayout:${Versions.constraintLayout}"

    //test libs
    private val junit = "junit:junit:${Versions.junit}"
    private val extJUnit = "androidx.test.ext:junit:${Versions.extJunit}"
    private val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}"

    val appLibraries = arrayListOf<String>().apply {
        add(kotlinStdLib)
        add(coreKtx)
        add(appcompat)
        add(constraintLayout)
    }

    val androidTestLibraries = arrayListOf<String>().apply {
        add(extJUnit)
        add(espressoCore)
    }

    val testLibraries = arrayListOf<String>().apply {
        add(junit)
    }
}

//util functions for adding the different type dependencies from build.gradle file
fun DependencyHandler.kapt(list: List<String>) {
    list.forEach { dependency ->
        add("kapt", dependency)
    }
}

fun DependencyHandler.implementation(list: List<String>) {
    list.forEach { dependency ->
        add("implementation", dependency)
    }
}

fun DependencyHandler.androidTestImplementation(list: List<String>) {
    list.forEach { dependency ->
        add("androidTestImplementation", dependency)
    }
}

fun DependencyHandler.testImplementation(list: List<String>) {
    list.forEach { dependency ->
        add("testImplementation", dependency)
    }
}        

The dependencies for our app, including those related to UI, testing, and third-party libraries, are consolidated in a single file. This file also contains Kotlin extension functions for implementation, testImplementation, androidTestImplementation, and kapt. These functions now accept a list of String dependencies, enabling the addition of multiple dependencies at once instead of individually in the build.gradle file. Moreover, one can organize the dependencies based on the module name by creating a separate list of dependencies for each module and adding them all at once using a single line of code in the Gradle script. This approach streamlines the process of managing dependencies, enhancing code maintainability and productivity.

This is what the end result will look like after adding all the Kotlin files.

No alt text provided for this image
Structure buildSrc folder

Furthermore, it's not an absolute requirement to manage all these constants in a single file. However, if your project is complex enough to drive you crazy with every small change, it's advisable to separate your concerns into different files for different purposes. Let's make the conversion.

settings.gradle

Below is the code for our current settings.gradle file.

include ':app:repository'
include ':app:core'
include ':app'
rootProject.name = "DSL Android"        

settings.gradle.kts

include(":repository", ":core", ":app")
rootProject.name = "DSL Android"        

The include function now accepts a variable number of String arguments to include the necessary modules in the project. Next, we will proceed to modify the build.gradle file at the project level.

build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = "1.3.72"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.0.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}        

build.gradle.kts

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:${Versions.gradle}")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlin}")
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

tasks.register("clean", Delete::class) {
    delete(rootProject.buildDir)
}        

The version of Kotlin DSL remains consistent as it utilizes DSL to map the syntaxes and aims to maintain close similarity with the original syntaxes.

classpath("com.android.tools.build:gradle:${Versions.gradle}")        

The "classpath" is a standard Kotlin function that receives a string as input. Its syntax is familiar and easy to comprehend. Now, let's take a look at how our main application-level "build.gradle" file is altered to "build.gradle.kts".

The app-level Gradle file is where plugins are defined to enable Android support in a regular IntelliJ project, Kotlin support in an Android project, or any necessary third-party plugins at the module level. By applying these plugins in the main Gradle file, we can observe the resulting changes in the project.

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'        

will be changed to

plugins{
    id("com.android.application")
    kotlin("android")
    kotlin("android.extensions")
}        

Let's break down the process of setting up an Android block into manageable chunks.

android {
    compileSdkVersion 30
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.maharshi.dslandroid"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

 //......
}        

will be changed to

android {
    compileSdkVersion(AppConfig.compileSdk)
    buildToolsVersion(AppConfig.buildToolsVersion)

    defaultConfig {
        applicationId = "com.maharshi.dslandroid"
        minSdkVersion(AppConfig.minSdk)
        targetSdkVersion(AppConfig.targetSdk)
        versionCode = AppConfig.versionCode
        versionName = AppConfig.versionName

        testInstrumentationRunner = AppConfig.androidTestInstrumentation
    }

 //......   
}        

It is now possible to directly access Kotlin object constants or any other constants, as shown above. In this example, we are utilizing the constants created earlier in the "AppConfig" file to configure the app at the application level. Next, we will explore how to create debug and release versions.

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

will be changed to

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

Debug mode is automatically available by default, so there's no need to create it. However, if you need to modify some properties, you can do so by following these steps.

getByName("debug") {
    isMinifyEnabled = false
}        

flavors:

flavorDimensions(AppConfig.dimension)
productFlavors {
    create("staging") {
        applicationIdSuffix = ".staging"
        setDimension(AppConfig.dimension)
    }

    create("production") {
        setDimension(AppConfig.dimension)
    }
}        

viewbinding:

viewBinding {
    android.buildFeatures.viewBinding = true
}        

coming to the main dependencies this is how it was like before

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.1'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

}        

will be now changed to below

dependencies {
    //std lib
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
    //app libs
    implementation(AppDependencies.appLibraries)
    //test libs
    testImplementation(AppDependencies.testLibraries)
    androidTestImplementation(AppDependencies.androidTestLibraries)
}        

This is highly maintainable and easily readable and comprehensible.

When you combine all of these elements, the result will look like the following.

plugins {
    id("com.android.application")
    kotlin("android")
    kotlin("android.extensions")
}

android {
    compileSdkVersion(AppConfig.compileSdk)
    buildToolsVersion(AppConfig.buildToolsVersion)

    defaultConfig {
        applicationId = "com.maharshi.dslandroid"
        minSdkVersion(AppConfig.minSdk)
        targetSdkVersion(AppConfig.targetSdk)
        versionCode = AppConfig.versionCode
        versionName = AppConfig.versionName

        testInstrumentationRunner = AppConfig.androidTestInstrumentation
    }

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

    flavorDimensions(AppConfig.dimension)
    productFlavors {
        create("staging") {
            applicationIdSuffix = ".staging"
            setDimension(AppConfig.dimension)
        }

        create("production") {
            setDimension(AppConfig.dimension)
        }
    }

    viewBinding {
        android.buildFeatures.viewBinding = true
    }

    packagingOptions {
        exclude("META-INF/notice.txt")
    }

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}

dependencies {
    //std lib
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
    //app libs
    implementation(AppDependencies.appLibraries)
    //test libs
    testImplementation(AppDependencies.testLibraries)
    androidTestImplementation(AppDependencies.androidTestLibraries)
}        

When converting an Android module, the process is almost the same as converting a regular Android app. However, instead of using an Android ID plugin, it's recommended to use the library plugin ID.

Kotlin DSL has a much wider range of uses beyond just Gradle scripts. It can be applied in various contexts and implementations. Let's explore the diverse applications of Kotlin DSL beyond just scripting.

Using Kotlin DSL to style a TextView.

To begin, let's create the foundation that will provide support for the DSL.

import android.graphics.Typeface
import android.widget.TextView

object StyleDSL {
? ? fun styleWith(style: TextView.() -> Unit) = style

? ? inline fun TextView.applyFont(typeface: Typeface) {
? ? ? ? this.typeface = typeface
? ? }

? ? inline fun TextView.changeTextSize(size: Float) {
? ? ? ? this.textSize = size
? ? }

? ? inline fun TextView.applyColor(color: Int) {
? ? ? ? this.setTextColor(color)
? ? }
}        

To use this class to style a TextView, you can use the styleWith function along with the inline functions to define the style of the TextView:

val myTextView = TextView(context).apply {
? ? text = "Hello, World!"
? ? styleWith {
? ? ? ? applyFont(Typeface.DEFAULT_BOLD)
? ? ? ? changeTextSize(24f)
? ? ? ? applyColor(Color.RED)
? ? }
}        

In this example, we create a new TextView and use the styleWith function to apply the style to the TextView. The inline functions applyFont, changeTextSize, and applyColor are used to define the font, text size, and color of the TextView, respectively.

By using a DSL like this, you can create reusable styles that can be applied to any TextView in your app, making it easy to maintain a consistent look and feel across your app.

This is a simple example of how Kotlin DSL can be used to express a wide variety of programming concepts in a clean and expressive way. By leveraging Kotlin's lambda expressions, we can create code that is easy to read and maintain.

However, it's important to be aware that using lambda expressions extensively can result in the generation of extra classes at the bytecode level. To address this issue, we can inline the lambda expressions wherever possible. By doing so, we can reduce the number of extra classes generated for small code blocks and improve the overall performance of our code.

That concludes today's post. Your comments, feedback, suggestions, or queries are greatly appreciated. Thank you for taking the time to read this! ??

Happy coding!

#kotlin #androiddevelopment #mobileappdevelopment #androiddeveloper


Stay up-to-date with the latest coding tips and tricks by subscribing to our newsletter at AppUp! 'N Grow. Connect with us on Instagram to inspire our team and foster collaboration, unlocking new achievements together with AppUp! 'N Grow.

Kuldeep Sakhiya

Team Lead at Mantra Softech

1 年

Moving forward, everything may be set by the Kotlin file! ????

要查看或添加评论,请登录

Maharshi Saparia的更多文章

社区洞察

其他会员也浏览了