Featured image of post Better Dependency Management Using buildSrc + Kotlin DSL

Better Dependency Management Using buildSrc + Kotlin DSL

Learning Micronaut Journey - Part 4

Understanding the Challenge

Let’s take a multi-module app to understand the problem. Manually declaring versions along with dependencies across multi modules is our first approach. That would look something like below

At first module build.gradle file we declare dependencies:

1
2
3
implementation "androidx.appcompat:appcompat:1.0.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41"
xxxxxx

In the second module build.gradle fiile we declare same dependencies:

1
2
3
implementation "androidx.appcompat:appcompat:1.0.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.41"
xxxxxxxx

As we are defining dependency versions at each build.gradle file this has created problems like conflicting versions if not upgraded correctly. It’s very hard to manage the version upgrades of dependencies and config data. Whenever there is a change in dependency if we have more modules there was a lot of manual process of going to each and every module and checking whether the dependency is exiting or not if found updating and then moving to next.

What is a DSL and Kotlin DSL?

What is a DSL?

DSL is an acronym for Domain Specific Language that can be used in the context of a particular domain. It’s a contrast to General-Purpose Language (GPL) like Java which is widely applicable or used for multiple domains. It helps us to write declarative code to reduce the boilerplate stuff. The code written with DSL would be much easier to read.

The common usage of DSL language is HTML in Web development, Gradle in build tools, SQL in data management, XML for the Markup language, etc. Though we might have experience in above mentioned one or more languages but we might not know that we are using DSL.

Kotlin DSL?

Kotlin DSL is built on top of the core language Kotlin. So the syntax would no different from the parent language which gives us the benefit of using Kotlin for development.

Introducing buildSrc

buildSrc is a directory at the project root level which contains build info. We can use this directory to enable kotlin-dsl and write logic related to custom configuration and share them across the project. It was one of the most used approaches in recent days because of its testability

The directory buildSrc is treated as an included build. Upon discovery of the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script. For multi-project builds there can be only one buildSrc directory, which has to sit in the root project directory. buildSrc should be preferred over script plugins as it is easier to maintain, refactor and test the code. — Gradle Docs

Setting Up buildSrc

  1. Create a directory named buildSrc in the root of your Micronaut project.

  2. Inside buildSrc, create a directory structure mirroring the package of your build logic. For example, if your build logic is for dependency management, you might create a package like com.melon.dependency.

  3. Create a build.gradle.kts file and add the following code to it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib")
}
  1. Create a settings.gradle.kts file and add the following code to it:
1
rootProject.name = "buildSrc"
  1. The structure will be as shown below:

  1. Create Libraries and Versions objects inside buildSrc/src/main/kotlin/com/melon/dependency/
  • Update all libraries inside the Libraries object:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.melon.dependency

object Libraries {

    object Kotlin {
        const val stdlib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"
        const val stdlibJdk8 = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}"
        const val reflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}"
    }

    object Logging {
        const val logbackClassic = "ch.qos.logback:logback-classic:1.4.11"
    }

    object Micronaut {
        const val httpClient = "io.micronaut:micronaut-http-client:${Versions.Micronaut.core}"
        const val httpValidation = "io.micronaut:micronaut-http-validation:${Versions.Micronaut.core}"

        object Kotlin {
            const val runtime = "io.micronaut.kotlin:micronaut-kotlin-runtime:${Versions.Micronaut.kotlin}"
        }

        object Serde {
            const val jackson = "io.micronaut.serde:micronaut-serde-jackson:${Versions.Micronaut.serde}"
            const val processor = "io.micronaut.serde:micronaut-serde-processor:${Versions.Micronaut.serde}"
        }
    }

    object Jackson {
        const val kotlin = "com.fasterxml.jackson.module:jackson-module-kotlin:${Versions.jackson}"
    }
}
  • Update all versions inside the Versions object:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.melon.dependency

object Versions {
    const val kotlin = "1.9.10"
    const val jackson = "2.15.3"

    object  Micronaut {
        const val core = "4.2.0"
        const val serde = "2.4.0"
        const val kotlin = "4.1.0"
    }
}
  1. Update the build.gradle file inside the app/api/ directory of the project:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
dependencies {
    ksp(Libraries.Micronaut.httpValidation)
    ksp(Libraries.Micronaut.Serde.processor)
    implementation(Libraries.Micronaut.Kotlin.runtime)
    implementation(Libraries.Micronaut.Serde.jackson)
    implementation(Libraries.Kotlin.reflect)
    implementation(Libraries.Kotlin.stdlibJdk8)
    compileOnly(Libraries.Micronaut.httpClient)
    runtimeOnly(Libraries.Logging.logbackClassic)
    runtimeOnly(Libraries.Jackson.kotlin)
    testImplementation(Libraries.Micronaut.httpClient)
}

Conclusion

buildSrc + Kotlin DSL is the best option for dependency management. As it’s a class-level declaration it can be easily tested. The auto-suggestion support and code navigation would help in saving time. Maintain separate classes for each purpose. This approach could be easy for better reusability and easy maintenance.

References

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy